├── .env.example ├── .gitignore ├── LICENSE ├── README.md ├── assets ├── chartjs-logo-color.png ├── datadoc_logo.png ├── docker-logo-color.png ├── electron-logo-color.png ├── express-logo-color.png ├── influxdb-logo-color.png ├── material-ui-logo-color.png ├── node-logo-color.png ├── npm-logo-color.png ├── postgres-logo-color.png ├── react-logo-color.png ├── twilio-logo-color.png └── webpack-logo-color.png ├── client ├── components │ ├── DonutChart.jsx │ ├── FlashError.jsx │ ├── Forward.jsx │ ├── Histogram.jsx │ ├── Home.jsx │ ├── HomeButton.jsx │ ├── LineChart.jsx │ ├── LogTable.jsx │ ├── NavButtons.jsx │ ├── SearchBar.jsx │ ├── Settings.jsx │ ├── URI.jsx │ ├── URITable.jsx │ ├── WorkspaceCard.jsx │ └── WorkspaceInfo.jsx ├── containers │ ├── App.jsx │ ├── ChartsContainer.jsx │ ├── Dashboard.jsx │ ├── DrawerContents.jsx │ ├── Header.jsx │ ├── NavBar.jsx │ ├── Production.jsx │ ├── Sidebar.jsx │ ├── SimulationView.jsx │ ├── Topbar.jsx │ └── WorkspaceView.jsx ├── index.js ├── styles │ ├── AddWorkspace.scss │ ├── Charts.scss │ ├── Header.scss │ ├── NavBar.scss │ ├── Settings.scss │ ├── WorkspaceBox.scss │ └── globals.scss └── theme.js ├── docker-compose.yml ├── dummy ├── .gcloudignore ├── api.js ├── app.yaml ├── assets │ └── favicon.ico ├── index.html ├── module │ ├── index.js │ ├── package-lock.json │ └── package.json ├── package-lock.json ├── package.json └── server.js ├── electron └── electron-main.js ├── examples ├── endpoint_view.png ├── home_view.png ├── intro.png ├── monitoring_view.png └── simulation_view.png ├── package-lock.json ├── package.json ├── server ├── controllers │ ├── influxController.js │ └── pgController.js ├── models │ ├── influx-client.js │ ├── postgres-client.js │ └── postgres-init.sql ├── routes │ ├── chartdata.js │ └── logRouter.js ├── server.js └── twilio │ ├── email.js │ └── twilio.js ├── template.html └── webpack.config.js /.env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV= 2 | PORT= 3 | SERVER_URL= 4 | PG_HOST= 5 | PG_PORT= 6 | PG_USER= 7 | PG_PASS= 8 | PG_DB= 9 | DB_INFLUXDB_INIT_MODE= 10 | DB_INFLUXDB_INIT_USERNAME= 11 | DB_INFLUXDB_INIT_PASSWORD= 12 | DB_INFLUXDB_INIT_ORG= 13 | DB_INFLUXDB_INIT_BUCKET= 14 | DB_INFLUXDB_INIT_RETENTION= 15 | DB_INFLUXDB_INIT_ADMIN_TOKEN= 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules 3 | dist 4 | .DS_Store 5 | *.env 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 DataDoc 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 | # DataDoc 2 | 3 | DataDoc is an endpoint monitoring, detection and traffic simulation tool that provides real-time metrics and customizable alert notifications. 4 | 5 | 6 | 7 | ### Table of Contents 8 | * [Getting Started](#getting-started) 9 | * [Prerequisites](#prerequisites) 10 | * [Installation](#installation) 11 | * [How To Use](#how-to-use) 12 | * [Adding Workspaces](#adding-workspaces) 13 | * [Using the Monitoring Tool](#using-the-monitoring-tool) 14 | * [Using the Simulation Tool](#using-the-simulation-tool) 15 | * [Configuring Alerts](#configuring-alerts) 16 | * [Tech Stack](#tech-stack) 17 | * [Authors](#authors) 18 | 19 | ## Getting Started 20 | 21 | ### Prerequisites 22 | 23 | - Node.js v18^ 24 | - Docker 25 | 26 | ### Installation 27 | 28 | This tool requires the npm package `express-endpoints-monitor` to detect and gather metrics for endpoints in your Express application. To understand how to use this plugin, see the full documentation. 29 | 30 | 1. Run the following terminal command in your project directory that you would like to begin monitoring: 31 | 32 | ``` 33 | npm install express-endpoints-monitor 34 | ``` 35 | 36 | This should have created a `express-endpoints-monitor/` folder in your `node_modules/` directory 37 | 38 | 2. WIP: Clone this repository. Unzip the file in a separate folder and open a terminal in this directory. Run the following commands: 39 | 40 | ``` 41 | npm install 42 | npm run build 43 | ``` 44 | 45 | This will install the needed dependences and build the desktop application. 46 | 47 | ### Exposing Endpoints to the Monitoring Tool 48 | 49 | 1. Open your Express application file in a text editor. At the top of the file, import the plugin by adding: 50 | 51 | ``` 52 | const expMonitor = require("express-endpoints-monitor"); 53 | ``` 54 | 55 | This module comes with several functions to register endpoints with the monitoring application and begin log requests made to those endpoints. 56 | 57 | 2. In your file, include the following line: 58 | 59 | ``` 60 | app.use(expMonitor.gatherMetrics); 61 | ``` 62 | 63 | This will record metrics for incoming requests and make them available to the metrics API which will be set up later. 64 | 65 | 3. Under an endpoint that you would like to begin monitoring, include the `expMonitor.registerEndpoint` middleware. For example, this may look like: 66 | 67 | ``` 68 | app.get(..., 69 | expMonitor.registerEndpoint, 70 | ... 71 | ); 72 | ``` 73 | 74 | The order of this function in the middleware chain is not important. This middleware will stage this particular endpoint for exporting, and can be used in multiple endpoints. 75 | 76 | 4. Once all desired endpoints have been registered, they must be exported on the metrics server. In your `app.listen` declaration, add these lines to the passed-in callback function: 77 | 78 | ``` 79 | app.listen(..., function callback() { 80 | ... 81 | expMonitor.exportEndpoints(); 82 | startMetricsServer() 83 | ) 84 | ``` 85 | 86 | This will start up a metrics server on `METRICS_SERVER_PORT`. If this argument is not specified, it will resolve to `9991`. The server includes several endpoints, one of which is `GET /endpoints` which responds with the list of registered endpoints in JSON format. 87 | 88 | Alternatively, if you would like to export all endpoints, you may replace the above snippet with the `exportAllEndpoints` function: 89 | 90 | ``` 91 | app.listen(..., function callback() { 92 | ... 93 | expMonitor.exportAllEndpoints(); 94 | startMetricsServer() 95 | ) 96 | ``` 97 | 98 | This will expose all endpoints regardless of whether they include the `registerEndpoint` middleware. 99 | 100 | 5. Your application is ready to start monitoring! To verify your setup, use a browser or API testing tool to interact with the metrics API started at `http://localhost:`. The list of available endpoints is: 101 | 102 | - `GET /endpoints` 103 | - `GET /metrics` 104 | - `DELETE /metrics` 105 | 106 | 6. To see the full use of the library, see the npm page. 107 | 108 | ### Initializing Databases 109 | 110 | In your local `DataDoc` folder, run the following command: 111 | 112 | ``` 113 | docker compose up 114 | ``` 115 | 116 | The `-d` flag may be supplied to detach the instance from the terminal. 117 | 118 | ### Starting the Desktop Application 119 | 120 | 1. In your local `DataDoc` folder, run the following command if you haven't during the installation steps: 121 | 122 | ``` 123 | npm build 124 | ``` 125 | 126 | This command only needs to be run once. 127 | 128 | 2. WIP: In the same folder, run the following command to start the desktop application: 129 | 130 | ``` 131 | npm start 132 | ``` 133 | ## How to Use 134 | 135 | ### Adding Workspaces 136 | 137 | ### Using the Monitoring Tool 138 | 139 | ### Using the Simulation Tool 140 | 141 | ### Configuring Alerts 142 | 143 | ## Tech Stack 144 | Electron 145 | 146 | React 147 | 148 | MaterialUI 149 | 150 | MaterialUI 151 | 152 | Express 153 | 154 | Node.js 155 | 156 | npm 157 | 158 | Postgres 159 | 160 | InfluxDB 161 | 162 | Twilio 163 | 164 | Docker 165 | 166 | Webpack 167 | 168 | ## Authors 169 | 170 | - Jo Huang LinkedIn | GitHub 171 | - Jonathan Huang LinkedIn | GitHub 172 | - Jamie Schiff LinkedIn | GitHub 173 | - Mariam Zakariadze LinkedIn | GitHub 174 | -------------------------------------------------------------------------------- /assets/chartjs-logo-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/DataDoc/b3ee07ce10363eb11cbe0a770c0f7b336fd0c14c/assets/chartjs-logo-color.png -------------------------------------------------------------------------------- /assets/datadoc_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/DataDoc/b3ee07ce10363eb11cbe0a770c0f7b336fd0c14c/assets/datadoc_logo.png -------------------------------------------------------------------------------- /assets/docker-logo-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/DataDoc/b3ee07ce10363eb11cbe0a770c0f7b336fd0c14c/assets/docker-logo-color.png -------------------------------------------------------------------------------- /assets/electron-logo-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/DataDoc/b3ee07ce10363eb11cbe0a770c0f7b336fd0c14c/assets/electron-logo-color.png -------------------------------------------------------------------------------- /assets/express-logo-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/DataDoc/b3ee07ce10363eb11cbe0a770c0f7b336fd0c14c/assets/express-logo-color.png -------------------------------------------------------------------------------- /assets/influxdb-logo-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/DataDoc/b3ee07ce10363eb11cbe0a770c0f7b336fd0c14c/assets/influxdb-logo-color.png -------------------------------------------------------------------------------- /assets/material-ui-logo-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/DataDoc/b3ee07ce10363eb11cbe0a770c0f7b336fd0c14c/assets/material-ui-logo-color.png -------------------------------------------------------------------------------- /assets/node-logo-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/DataDoc/b3ee07ce10363eb11cbe0a770c0f7b336fd0c14c/assets/node-logo-color.png -------------------------------------------------------------------------------- /assets/npm-logo-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/DataDoc/b3ee07ce10363eb11cbe0a770c0f7b336fd0c14c/assets/npm-logo-color.png -------------------------------------------------------------------------------- /assets/postgres-logo-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/DataDoc/b3ee07ce10363eb11cbe0a770c0f7b336fd0c14c/assets/postgres-logo-color.png -------------------------------------------------------------------------------- /assets/react-logo-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/DataDoc/b3ee07ce10363eb11cbe0a770c0f7b336fd0c14c/assets/react-logo-color.png -------------------------------------------------------------------------------- /assets/twilio-logo-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/DataDoc/b3ee07ce10363eb11cbe0a770c0f7b336fd0c14c/assets/twilio-logo-color.png -------------------------------------------------------------------------------- /assets/webpack-logo-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/DataDoc/b3ee07ce10363eb11cbe0a770c0f7b336fd0c14c/assets/webpack-logo-color.png -------------------------------------------------------------------------------- /client/components/DonutChart.jsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from "@mui/material"; 2 | import { ArcElement, Chart as ChartJS, Legend, Title, Tooltip } from "chart.js"; 3 | import React from "react"; 4 | import { Doughnut } from "react-chartjs-2"; 5 | import { tokens } from "../theme.js"; 6 | 7 | const DonutChart = (props) => { 8 | const { id, chartData } = props; 9 | const theme = useTheme(); 10 | const colors = tokens(theme.palette.mode); 11 | 12 | ChartJS.register(ArcElement, Tooltip, Title, Legend); 13 | 14 | const backgroundOpacity = 0.7; 15 | const borderOpacity = 0.25; 16 | const gradientFactor = 4; 17 | const colorMapper = (value, gradientFactor, opacity) => { 18 | if (value === "N/A") { 19 | return `rgba(192, 192, 192, ${opacity})` 20 | } 21 | else if (value < 200) 22 | return `rgba(64, ${ 23 | 192 - (Number(value) % 100) * gradientFactor 24 | }, 192, ${opacity})`; 25 | else if (value < 300) 26 | return `rgba(64, ${ 27 | 255 - (Number(value) % 200) * gradientFactor 28 | }, 64, ${opacity})`; 29 | else if (value < 400) 30 | return `rgba(${255 - (Number(value) % 300) * gradientFactor},${ 31 | 255 - (Number(value) % 300) * gradientFactor 32 | }, 128, ${opacity})`; 33 | else if (value < 500) 34 | return `rgba(${ 35 | 255 - (Number(value) % 400) * gradientFactor 36 | }, 64, 64, ${opacity})`; 37 | else if (value < 600) 38 | return `rgba(${ 39 | 128 - (Number(value) % 500) * gradientFactor 40 | }, 64, 255, ${opacity})`; 41 | }; 42 | 43 | const convertCountToPercentage = (counts) => { 44 | const total = counts.reduce((curr, acc) => curr + acc, 0); 45 | return counts.map(count => count / total * 100); 46 | } 47 | 48 | const data = { 49 | labels: chartData.map((point) => String(point.x)), 50 | datasets: [ 51 | { 52 | label: "Percentage", 53 | data: convertCountToPercentage(chartData.map((point) => point.y)), 54 | backgroundColor: chartData 55 | .map((point) => point.x) 56 | .map((status_code) => 57 | colorMapper(status_code, gradientFactor, backgroundOpacity) 58 | ), 59 | borderColor: chartData 60 | .map((point) => point.x) 61 | .map((status_code) => 62 | colorMapper(status_code, gradientFactor, borderOpacity) 63 | ), 64 | borderWidth: 1, 65 | }, 66 | ], 67 | }; 68 | 69 | const options = { 70 | responsive: true, 71 | maintainAspectRatio: false, 72 | resizeDelay: 200, 73 | plugins: { 74 | title: { 75 | display: true, 76 | text: 'Status Code Distribution', 77 | }, 78 | legend: { 79 | display: true, 80 | position: "left", 81 | align: "center", 82 | labels: { 83 | padding: 0, 84 | } 85 | }, 86 | }, 87 | }; 88 | 89 | return ( 90 | //
91 | 99 | //
100 | ); 101 | }; 102 | 103 | export default DonutChart; 104 | -------------------------------------------------------------------------------- /client/components/FlashError.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const FlashError = (props) => { 4 | const { errorMessage } = props; 5 | console.log(errorMessage) 6 | return ( 7 |
8 |
{errorMessage}
9 |
10 | ); 11 | }; 12 | export default FlashError; 13 | -------------------------------------------------------------------------------- /client/components/Forward.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useNavigate } from "react-router-dom"; 3 | 4 | const Forward = () => { 5 | let navigate = useNavigate(); 6 | return ( 7 | <> 8 | 9 | 10 | ); 11 | }; 12 | 13 | export default Forward; 14 | -------------------------------------------------------------------------------- /client/components/Histogram.jsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from "@mui/material"; 2 | import React from "react"; 3 | import { tokens } from "../theme"; 4 | 5 | import { 6 | BarElement, CategoryScale, Chart as ChartJS, Legend, LinearScale, Title, 7 | Tooltip 8 | } from "chart.js"; 9 | import { Bar } from "react-chartjs-2"; 10 | 11 | const Histogram = (props) => { 12 | 13 | const theme = useTheme(); 14 | const colors = tokens(theme.palette.mode); 15 | 16 | const removeEmptyBins = (histData) => { 17 | const newData = []; 18 | for (let i = 0; i < histData.length; i++) { 19 | const someInLaterBins = histData.slice((i > 0 ? i - 1 : 0)).some((dataPoint) => dataPoint.y > 0) 20 | if (someInLaterBins) { 21 | newData.push(histData[i]); 22 | } else { 23 | break; 24 | } 25 | } 26 | return newData; 27 | }; 28 | 29 | const chartData = removeEmptyBins(props.chartData) || []; 30 | 31 | ChartJS.register( 32 | CategoryScale, 33 | LinearScale, 34 | BarElement, 35 | Title, 36 | Tooltip, 37 | Legend 38 | ); 39 | 40 | const data = { 41 | labels: chartData.map((point) => point.x), 42 | datasets: [ 43 | { 44 | label: "Frequency", 45 | data: chartData.map((point) => point.y), 46 | backgroundColor: chartData 47 | .map((point) => point.x) 48 | .map((e, i) => { 49 | return `rgba(${64 + 32 * i}, ${255 - 16 * i}, 64, 0.8)`; 50 | }), 51 | barPercentage: 1.0, 52 | categoryPercentage: 1.0, 53 | borderWidth: 1.0 54 | } 55 | ] 56 | }; 57 | 58 | const options = { 59 | responsive: true, 60 | maintainAspectRatio: false, 61 | resizeDelay: 200, 62 | plugins: { 63 | legend: { 64 | display: false, 65 | position: "top" 66 | }, 67 | title: { 68 | display: true, 69 | text: "Response Time Distribution" 70 | } 71 | } 72 | }; 73 | 74 | return ( 75 | //
76 | 84 | //
85 | ); 86 | }; 87 | 88 | export default Histogram; 89 | -------------------------------------------------------------------------------- /client/components/Home.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { Box, Card, Container, Typography } from "@mui/material"; 3 | import { Add } from "@mui/icons-material"; 4 | import Grid from "@mui/material/Unstable_Grid2"; 5 | import WorkspaceCard from "./WorkspaceCard.jsx"; 6 | import "../styles/AddWorkspace.scss"; 7 | import { useTheme } from "@mui/material"; 8 | import { tokens } from "../theme.js"; 9 | 10 | const { SERVER_URL } = process.env; 11 | 12 | const Home = (props) => { 13 | const theme = useTheme(); 14 | const colors = tokens(theme.palette.mode); 15 | const [workspaceList, setWorkspaceList] = useState([]); 16 | const [showNewWorkspacePopUp, setShowNewWorkspacePopUp] = useState(false); 17 | const [workspaceValues, setWorkspaceValues] = useState({ 18 | name: "", 19 | domain: "", 20 | port: "", 21 | workspaceId: "", 22 | }); 23 | 24 | const handleChange = (e, updateValue) => { 25 | let updatedState; 26 | let workspaceUpdate; 27 | workspaceUpdate = { [updateValue]: e.target.value }; 28 | updatedState = { 29 | ...workspaceValues, 30 | ...workspaceUpdate 31 | }; 32 | setWorkspaceValues(updatedState); 33 | }; 34 | 35 | const handleSubmit = (e) => { 36 | e.preventDefault(); 37 | fetch(`${SERVER_URL}/workspaces`, { 38 | method: "POST", 39 | headers: { "Content-Type": "application/json" }, 40 | body: JSON.stringify(workspaceValues) 41 | }) 42 | .then(() => { 43 | getAllWorkspaces(); 44 | }) 45 | .then(() => { 46 | setWorkspaceValues({ 47 | name: "", 48 | domain: "", 49 | port: 0 50 | }); 51 | }) 52 | .then(() => { 53 | setShowNewWorkspacePopUp(false); 54 | }) 55 | .catch((err) => { 56 | console.log( 57 | `there wan an error submitting a new workspace, err: ${err}` 58 | ); 59 | }); 60 | }; 61 | 62 | //set the values for the new workspace 63 | const newWorkspaceForm = ( 64 |
handleSubmit(e)}> 65 |
66 | 72 |

Add a new workspace:

73 | 74 | handleChange(e, "name")} 79 | required 80 | > 81 | 82 | handleChange(e, "domain")} 87 | required 88 | > 89 | 90 | handleChange(e, "port")} 95 | required 96 | > 97 | 98 | handleChange(e, "metricsPort")} 103 | required 104 | > 105 | 113 |
114 |
115 | ); 116 | 117 | //fetch the workspace list from the backend when the component mounts 118 | useEffect(() => { 119 | getAllWorkspaces(); 120 | }, []); 121 | 122 | const getAllWorkspaces = () => { 123 | fetch(`http://localhost:${process.env.PORT}/workspaces`) 124 | .then((response) => { 125 | return response.json(); 126 | }) 127 | .then((data) => { 128 | setWorkspaceList(data); 129 | }) 130 | .catch((err) => { 131 | console.log(`there was an error: ${err}`); 132 | }); 133 | }; 134 | 135 | const deleteWorkspaceById = (workspaceId) => { 136 | fetch(`${SERVER_URL}/workspaces`, { 137 | method: "DELETE", 138 | headers: { 139 | "Content-Type": "application/json" 140 | }, 141 | body: JSON.stringify({ workspace_id: workspaceId }) 142 | }) 143 | .then((workspace) => { 144 | getAllWorkspaces(); 145 | }) 146 | .catch((err) => { 147 | console.log(`there was an error deleting a workspace, error: ${err}`); 148 | }); 149 | }; 150 | 151 | // const deleteSpecificWorkspace = (name) => { 152 | // // console.log("in the process of deleting a workspace"); 153 | // const updatedWorkspaceList = workspaceList.filter( 154 | // (item) => item.name !== name 155 | // ); 156 | // getWorkSpaceList(); 157 | // }; 158 | 159 | const cardStyle = { 160 | // boxSizing: 'border-box', 161 | borderRadius: 3, 162 | height: "170px", 163 | minwidth: "60px", 164 | padding: 2, 165 | // boxShadow: "0px 0px 8px 4px rgba(0, 0, 0, 0.02)", 166 | cursor: "pointer", 167 | backgroundColor: `${colors.secondary[100]}` 168 | }; 169 | 170 | return ( 171 | <> 172 | 173 | Welcome to DataDoc 174 | 175 | 176 | Workspaces: 177 | 178 | 179 | {workspaceList.map((workspace) => { 180 | return ( 181 | 190 | 191 | 199 | 200 | 201 | ); 202 | })} 203 | 204 | setShowNewWorkspacePopUp(true)} 213 | > 214 | 217 | {showNewWorkspacePopUp && newWorkspaceForm} 218 | 219 | 220 | 221 | 222 | ); 223 | }; 224 | 225 | export default Home; 226 | -------------------------------------------------------------------------------- /client/components/HomeButton.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { MemoryRouter as Router } from "react-router-dom"; 3 | 4 | // import Production from '../containers/Production' 5 | 6 | const HomeButton = (props) => { 7 | 8 | const { onClick } = props; 9 | 10 | return ( 11 | 16 | ); 17 | }; 18 | 19 | export default HomeButton; 20 | -------------------------------------------------------------------------------- /client/components/LineChart.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | Chart as ChartJS, Filler, Legend, LinearScale, LineElement, PointElement, TimeScale, Title, 3 | Tooltip 4 | } from "chart.js"; 5 | import "chartjs-adapter-moment"; 6 | import React from "react"; 7 | import { Line } from "react-chartjs-2"; 8 | 9 | const LineChart = (props) => { 10 | const { chartData, chartTitle, chartLabel } = props; 11 | 12 | ChartJS.register( 13 | TimeScale, 14 | LinearScale, 15 | PointElement, 16 | LineElement, 17 | Title, 18 | Tooltip, 19 | Legend, 20 | Filler 21 | ); 22 | 23 | const data = { 24 | datasets: [ 25 | { 26 | label: chartLabel, 27 | data: chartData, 28 | fill: true, 29 | backgroundColor: ["rgba(75, 192, 192, 0.50)"], 30 | borderColor: ["rgb(75, 192, 192)"], 31 | tension: 0.3, 32 | }, 33 | ], 34 | }; 35 | 36 | const options = { 37 | responsive: true, 38 | maintainAspectRatio: false, 39 | resizeDelay: 200, 40 | plugins: { 41 | title: { 42 | display: true, 43 | text: chartTitle, 44 | }, 45 | legend: { 46 | display: false, 47 | position: "bottom" 48 | }, 49 | }, 50 | maintainAspectRatio: false, 51 | scales: { 52 | x: { 53 | type: "time", 54 | grid: { 55 | display: false, 56 | } 57 | }, 58 | y: { 59 | beginAtZero: true, 60 | ticks: { 61 | stepSize: (() => { 62 | if (! chartData) return 1; 63 | const maxYValue = Math.max(...(chartData.map((point) => point.y))); 64 | // console.table(chartData.map((point) => point.y)); 65 | for (const stepSize of [0.1, 0.2, 0.5, 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000]) { 66 | if (maxYValue / stepSize < 6) return stepSize; 67 | } 68 | })() 69 | } 70 | } 71 | }, 72 | animation: { 73 | duration: 1000, 74 | // easing: "linear", 75 | }, 76 | animations: { 77 | x: { 78 | duration: 100, 79 | easing: "linear", 80 | }, 81 | y: { 82 | duration: 0, 83 | }, 84 | } 85 | }; 86 | 87 | return ( 88 | // <> 89 | // {/*
*/} 90 | // {/*
*/} 91 | //
95 | 104 | //
105 | // {/*
*/} 106 | // {/*
*/} 107 | // 108 | ); 109 | }; 110 | 111 | export default LineChart; 112 | -------------------------------------------------------------------------------- /client/components/LogTable.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | const { SERVER_URL } = process.env; 4 | 5 | const LogTable = (props) => { 6 | const { method, path, isMonitoring } = props; 7 | 8 | let [logEntries, setLogEntries] = useState([]); 9 | 10 | if (isMonitoring) { 11 | setTimeout(async () => { 12 | const encodedPath = path.replaceAll("/", "%2F"); 13 | setLogEntries( 14 | ( 15 | await ( 16 | await fetch( 17 | `${SERVER_URL}/logdata/?method=${method}&path=${encodedPath}` 18 | ) 19 | ).json() 20 | ).map((log) => { 21 | return ( 22 | 30 | ); 31 | }) 32 | ); 33 | }, 2000); 34 | } 35 | 36 | return ( 37 | <> 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | {logEntries} 49 |
TimePathMethodResponse TimeStatus Code
50 | 51 | ); 52 | }; 53 | 54 | const LogEntry = (props) => { 55 | const { timestamp, path, method, res_time, status_code } = props; 56 | 57 | return ( 58 | 59 | {timestamp} 60 | {path} 61 | {method} 62 | {res_time} 63 | {status_code} 64 | 65 | ); 66 | }; 67 | 68 | export default LogTable; 69 | -------------------------------------------------------------------------------- /client/components/NavButtons.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useNavigate } from "react-router-dom"; 3 | import { IconButton } from "@mui/material"; 4 | import { ArrowBack, ArrowForward } from '@mui/icons-material'; 5 | 6 | export const Back = () => { 7 | let navigate = useNavigate(); 8 | return ( 9 | navigate(-1)}> 10 | 11 | 12 | ); 13 | }; 14 | 15 | export const Forward = () => { 16 | let navigate = useNavigate(); 17 | return ( 18 | navigate(+1)}> 19 | 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /client/components/SearchBar.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { Input, TextField } from "@mui/material" 3 | import { Search } from "@mui/icons-material" 4 | 5 | const SearchBar = (props) => { 6 | 7 | const { handleSearchChange } = props; 8 | 9 | return ( 10 | <> 11 |
12 | } 19 | disableUnderline={true} 20 | sx={{ 21 | width: 300, 22 | px: 1, 23 | border: "0.5px solid", 24 | borderRadius: 2 25 | }} 26 | /> 27 |
28 | 29 | ); 30 | }; 31 | 32 | export default SearchBar; 33 | -------------------------------------------------------------------------------- /client/components/Settings.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import "../styles/Settings.scss"; 3 | 4 | const { SERVER_URL } = process.env; 5 | 6 | const Settings = (props) => { 7 | const { 8 | showsettingspopup: showSettingsPopup, 9 | setshowsettingspopup: setShowSettingsPopup 10 | } = props; 11 | const [settingValues, setSettingValues] = useState({ 12 | subscribers: [], 13 | status300: false, 14 | status400: false, 15 | status500: false 16 | }); 17 | 18 | function handleChange(e, updateValue) { 19 | let settingsUpdate; 20 | let updatedState; 21 | if ( 22 | updateValue === "status300" || 23 | updateValue === "status400" || 24 | updateValue === "status500" 25 | ) { 26 | settingsUpdate = { [updateValue]: e.target.checked }; 27 | updatedState = { 28 | ...settingValues, 29 | ...settingsUpdate 30 | }; 31 | } else { 32 | settingsUpdate = { [updateValue]: e.target.value }; 33 | updatedState = { 34 | ...settingValues, 35 | ...settingsUpdate 36 | }; 37 | } 38 | setSettingValues(updatedState); 39 | } 40 | //send a post request to update the settings 41 | function handleSubmit(e) { 42 | e.preventDefault(); 43 | fetch(`${SERVER_URL}/registration`, { 44 | method: "POST", 45 | headers: { "Content-Type": "application/json" }, 46 | body: JSON.stringify(settingValues) 47 | }).then(() => { 48 | setSettingValues({ 49 | subscribers: [], 50 | status300: false, 51 | status400: false, 52 | status500: false 53 | }); 54 | }); 55 | } 56 | //settings form 57 | const SettingsForm = () => ( 58 |
59 |
60 | 66 |

Settings

67 | 68 | handleChange(e, "subscribers")} 75 | > 76 | 77 | 80 | 81 | handleChange(e, "status300")} 86 | > 87 | 88 | 89 | 90 | handleChange(e, "status400")} 95 | > 96 | 97 | 98 | 99 | handleChange(e, "status500")} 104 | > 105 | 106 | 107 |
108 | 115 |
116 |
117 | ); 118 | return ( 119 |
{showSettingsPopup && }
120 | ); 121 | }; 122 | 123 | export default Settings; 124 | -------------------------------------------------------------------------------- /client/components/URI.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useCallback } from "react"; 2 | import { Link, useNavigate } from "react-router-dom"; 3 | 4 | const URI = (props) => { 5 | const { id, path, method, status, checked, addToTracking, removeFromTracking } = props; 6 | 7 | const handleClick = () => { 8 | if (checked) removeFromTracking(props.method, props.path); 9 | else addToTracking(props.method, props.path); 10 | return; 11 | }; 12 | 13 | return ( 14 | <> 15 | 16 | 17 | 23 | 24 | 25 | 35 | {path} 36 | 37 | 38 | {method} 39 | 40 | 200 && status < 400 45 | ? { backgroundColor: "yellow" } 46 | : { backgroundColor: "red" } 47 | } 48 | > 49 | {status} 50 | 51 | 52 | 53 | 57 | 58 | 59 | 60 | ); 61 | }; 62 | 63 | export default URI; 64 | -------------------------------------------------------------------------------- /client/components/URITable.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { alpha } from "@mui/material/styles"; 4 | import { 5 | Box, 6 | Table, 7 | TableBody, 8 | TableCell, 9 | TableContainer, 10 | TableHead, 11 | TablePagination, 12 | TableRow, 13 | TableSortLabel, 14 | Toolbar, 15 | Typography, 16 | Paper, 17 | Checkbox, 18 | IconButton, 19 | Tooltip, 20 | FormControlLabel, 21 | Switch 22 | } from "@mui/material"; 23 | import { Delete, FilterList, Sync as Refresh } from "@mui/icons-material"; 24 | import { visuallyHidden } from "@mui/utils"; 25 | import { useNavigate } from "react-router-dom"; 26 | import SearchBar from "./SearchBar.jsx" 27 | 28 | function descendingComparator(a, b, orderBy) { 29 | if (b[orderBy] < a[orderBy]) { 30 | return -1; 31 | } 32 | if (b[orderBy] > a[orderBy]) { 33 | return 1; 34 | } 35 | return 0; 36 | } 37 | 38 | function getComparator(order, orderBy) { 39 | return order === "desc" 40 | ? (a, b) => descendingComparator(a, b, orderBy) 41 | : (a, b) => -descendingComparator(a, b, orderBy); 42 | } 43 | 44 | // Since 2020 all major browsers ensure sort stability with Array.prototype.sort(). 45 | // stableSort() brings sort stability to non-modern browsers (notably IE11). If you 46 | // only support modern browsers you can replace stableSort(exampleArray, exampleComparator) 47 | // with exampleArray.slice().sort(exampleComparator) 48 | function stableSort(array, comparator) { 49 | const stabilizedThis = array.map((el, index) => [el, index]); 50 | stabilizedThis.sort((a, b) => { 51 | const order = comparator(a[0], b[0]); 52 | if (order !== 0) { 53 | return order; 54 | } 55 | return a[1] - b[1]; 56 | }); 57 | return stabilizedThis.map((el) => el[0]); 58 | } 59 | 60 | const generateHeadCells = (rows) => { 61 | if (! rows?.length > 0) return []; 62 | return (Object.keys(rows[0])) 63 | .filter(key => key[0] !== '_') 64 | .map((key) => { 65 | return { 66 | id: key, 67 | numeric: typeof rows[key] === "number", 68 | disablePadding: true, 69 | label: key 70 | .replaceAll("_", " ") 71 | .split(" ") 72 | .map((word) => (word[0] ? word[0] : '').toUpperCase() + word.slice(1)) 73 | .join(" ") 74 | }; 75 | } 76 | ); 77 | }; 78 | 79 | function DataTableHead(props) { 80 | 81 | const { 82 | headCells, 83 | onSelectAllClick, 84 | order, 85 | orderBy, 86 | numSelected, 87 | rowCount, 88 | onRequestSort 89 | } = props; 90 | const createSortHandler = (property) => (event) => { 91 | onRequestSort(event, property); 92 | }; 93 | 94 | return ( 95 | 96 | 97 | 98 | 0 && numSelected < rowCount} 101 | checked={rowCount > 0 && numSelected === rowCount} 102 | onChange={onSelectAllClick} 103 | inputProps={{ 104 | "aria-label": "select all desserts" 105 | }} 106 | /> 107 | 108 | {headCells.map((headCell) => ( 109 | 116 | 121 | {headCell.label} 122 | {orderBy === headCell.id ? ( 123 | 124 | {order === "desc" ? "sorted descending" : "sorted ascending"} 125 | 126 | ) : null} 127 | 128 | 129 | ))} 130 | 131 | 132 | ); 133 | } 134 | 135 | DataTableHead.propTypes = { 136 | numSelected: PropTypes.number.isRequired, 137 | onRequestSort: PropTypes.func.isRequired, 138 | onSelectAllClick: PropTypes.func.isRequired, 139 | order: PropTypes.oneOf(["asc", "desc"]).isRequired, 140 | orderBy: PropTypes.string.isRequired, 141 | rowCount: PropTypes.number.isRequired 142 | }; 143 | 144 | function DataTableToolbar(props) { 145 | const { numSelected, metricsPort, refreshURIList, workspaceId, searchQuery, setSearchQuery, handleSearchChange } = props; 146 | 147 | return ( 148 | 0 && { 153 | bgcolor: (theme) => 154 | alpha( 155 | theme.palette.primary.main, 156 | theme.palette.action.activatedOpacity 157 | ) 158 | }) 159 | }} 160 | > 161 | {numSelected > 0 ? ( 162 | 168 | {numSelected} selected 169 | 170 | ) : ( 171 | 179 | 186 | 191 | Endpoints 192 | 193 | 194 | 199 | 204 | 205 | 212 | 213 | { 215 | // getURIListFromServer(props.metricsPort) 216 | refreshURIList(workspaceId, metricsPort); 217 | }} 218 | > 219 | 220 | 221 | 222 | 223 | 224 | )} 225 | {numSelected > 0 ? ( 226 | 227 | 228 | 229 | 230 | 231 | ) : ( 232 | <> 233 | 234 | 235 | )} 236 | 237 | 238 | ); 239 | } 240 | 241 | DataTableToolbar.propTypes = { 242 | numSelected: PropTypes.number.isRequired 243 | }; 244 | 245 | export default function URITable(props) { 246 | 247 | const { 248 | workspaceId, 249 | name, 250 | domain, 251 | port, 252 | metricsPort, 253 | rows, 254 | getURIListFromServer, 255 | updateTrackingInDatabaseById, 256 | refreshURIList, 257 | isMonitoring, 258 | setIsMonitoring 259 | } = props; 260 | 261 | const headCells = generateHeadCells(rows); 262 | 263 | const [searchQuery, setSearchQuery] = useState("") 264 | 265 | const handleSearchChange = (event) => { 266 | setSearchQuery(event.target.value); 267 | } 268 | 269 | const filterBySearchQuery = (unfilteredRows, searchQuery = "") => { 270 | return unfilteredRows.filter((row) => { 271 | return ( 272 | Object.keys(row || {}) 273 | .filter(columnName => columnName[0] !== '_') 274 | .some(column => row[column].toString().toLowerCase().includes(searchQuery.toLowerCase())) 275 | ) 276 | }) 277 | } 278 | 279 | const navigate = useNavigate(); 280 | 281 | const [order, setOrder] = React.useState("asc"); 282 | const [orderBy, setOrderBy] = React.useState("path"); 283 | const [selected, setSelected] = React.useState(rows.filter((row) => row.tracking)); 284 | const [page, setPage] = React.useState(0); 285 | const [dense, setDense] = React.useState(true); 286 | const [rowsPerPage, setRowsPerPage] = React.useState(10); 287 | 288 | const handleRequestSort = (event, property) => { 289 | const isAsc = orderBy === property && order === "asc"; 290 | setOrder(isAsc ? "desc" : "asc"); 291 | setOrderBy(property); 292 | }; 293 | 294 | const handleSelectAllClick = (event) => { 295 | if (event.target.checked) { 296 | const newSelected = rows.map((n) => n.name); 297 | setSelected(newSelected); 298 | return; 299 | } 300 | setSelected([]); 301 | }; 302 | 303 | const handleClick = (event, identifier) => { 304 | const selectedIndex = selected.indexOf(identifier); 305 | let newSelected = []; 306 | 307 | if (selectedIndex === -1) { 308 | newSelected = newSelected.concat(selected, identifier); 309 | } else if (selectedIndex === 0) { 310 | newSelected = newSelected.concat(selected.slice(1)); 311 | } else if (selectedIndex === selected.length - 1) { 312 | newSelected = newSelected.concat(selected.slice(0, -1)); 313 | } else if (selectedIndex > 0) { 314 | newSelected = newSelected.concat( 315 | selected.slice(0, selectedIndex), 316 | selected.slice(selectedIndex + 1) 317 | ); 318 | } 319 | 320 | setSelected(newSelected); 321 | }; 322 | 323 | const handleChangePage = (event, newPage) => { 324 | setPage(newPage); 325 | }; 326 | 327 | const handleChangeRowsPerPage = (event) => { 328 | setRowsPerPage(parseInt(event.target.value, 10)); 329 | setPage(0); 330 | }; 331 | 332 | const handleChangeDense = (event) => { 333 | setDense(event.target.checked); 334 | }; 335 | 336 | const isSelected = (identifier) => selected.indexOf(identifier) !== -1; 337 | 338 | // Avoid a layout jump when reaching the last page with empty rows. 339 | const emptyRows = 340 | page > 0 ? Math.max(0, (1 + page) * rowsPerPage - rows.length) : 0; 341 | 342 | return ( 343 | 344 | 345 | 355 | 356 | 361 | 370 | 371 | {filterBySearchQuery(stableSort(rows, getComparator(order, orderBy)), searchQuery) 372 | .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) 373 | .map((row, index) => { 374 | const isItemSelected = isSelected(row.id); 375 | const labelId = `enhanced-table-checkbox-${index}`; 376 | 377 | return ( 378 | handleClick(event, row.name)} 381 | role="checkbox" 382 | aria-checked={isItemSelected} 383 | tabIndex={-1} 384 | key={crypto.randomUUID()} 385 | selected={isItemSelected} 386 | > 387 | 388 | { 396 | row.tracking = ! row._tracking 397 | const newRow = Object.assign({}, { 398 | method: row.method, 399 | path: row.path, 400 | tracking: row.tracking, 401 | _id: row._id, 402 | }) 403 | try { 404 | updateTrackingInDatabaseById(newRow) 405 | } catch (err1) { 406 | console.error(err1); 407 | try { 408 | updateTrackingInDatabaseByRoute(newRow) 409 | } 410 | catch (err2) { 411 | console.error(err2); 412 | } 413 | } 414 | }} 415 | /> 416 | 417 | 418 | {Object.keys(row) 419 | .filter(key => key[0] !== '_') 420 | .map((column) => { 421 | return ( 422 | { 426 | if (column === "simulation" || column === "open") return; 427 | navigate(`/dashboard/${row._id}`, { state: { 428 | workspaceId, 429 | name, 430 | domain, 431 | port, 432 | metricsPort, 433 | endpointId: row._id, 434 | method: row.method, 435 | path: row.path, 436 | isMonitoring, 437 | }}) 438 | }} 439 | sx={{ cursor: "pointer" }} 440 | > 441 | {row[column]} 442 | 443 | ); 444 | }) 445 | } 446 | 447 | 448 | ); 449 | })} 450 | {emptyRows > 0 && ( 451 | 456 | 457 | 458 | )} 459 | 460 |
461 |
462 | 471 |
472 | {/* } 474 | label="Compact view" 475 | /> */} 476 |
477 | ); 478 | } 479 | -------------------------------------------------------------------------------- /client/components/WorkspaceCard.jsx: -------------------------------------------------------------------------------- 1 | import { Button, Typography } from "@mui/material"; 2 | import { useTheme } from "@mui/material/styles"; 3 | import React from "react"; 4 | import { useNavigate } from "react-router-dom"; 5 | import "../styles/WorkspaceBox.scss"; 6 | 7 | const WorkspaceCard = (props) => { 8 | const { workspaceId, name, domain, port, metricsPort, deleteWorkspace } = props; 9 | const theme = useTheme(); 10 | const navigate = useNavigate(); 11 | return ( 12 | <> 13 |
14 |
{ 16 | navigate(`/workspace/${workspaceId}`, { 17 | state: { 18 | workspaceId, 19 | name, 20 | domain, 21 | port, 22 | metricsPort, 23 | } 24 | })}} 25 | > 26 | 30 | {name} 31 | 32 | 35 | Domain: {domain} 36 | 37 | 41 | Port: {port || "N/A"} 42 | 43 |
44 |
45 | 52 |
53 |
54 | 55 | ); 56 | }; 57 | 58 | export default WorkspaceCard; 59 | -------------------------------------------------------------------------------- /client/components/WorkspaceInfo.jsx: -------------------------------------------------------------------------------- 1 | import { PlayArrow, Stop, TimerOutlined } from "@mui/icons-material"; 2 | import { 3 | Box, 4 | Button, 5 | ButtonGroup, 6 | Input, Typography 7 | } from "@mui/material"; 8 | import React, { useState } from "react"; 9 | import FlashError from "./FlashError.jsx"; 10 | 11 | const WorkspaceInfo = (props) => { 12 | const { 13 | URIList, 14 | setURIList, 15 | workspaceId, 16 | name, 17 | domain, 18 | port, 19 | metricsPort, 20 | isMonitoring, 21 | setIsMonitoring, 22 | getURIListFromServer 23 | } = props; 24 | const [errorMessage, setErrorMessage] = useState(""); 25 | const [searchInput, setSearchInput] = useState(""); 26 | const [pingInterval, setPingInterval] = useState(1); 27 | 28 | const minPingInterval = 0.5; 29 | 30 | // const inputHandler = (e) => { 31 | // // * Convert input text to lower case 32 | // let lowerCase = e.target.value.toLowerCase(); 33 | // setSearch(lowerCase); 34 | // }; 35 | 36 | // const getURIListFromServer = () => { 37 | // fetch( 38 | // `http://localhost:${process.env.PORT}/routes/server?metrics_port=${metricsPort}` 39 | // ) 40 | // .then((response) => response.json()) 41 | // .then((data) => { 42 | // setURIList(data); 43 | // }) 44 | // .catch((err) => { 45 | // setErrorMessage("Invalid server fetch request for the URI List"); 46 | // // * reset the error message 47 | // setTimeout(() => setErrorMessage(""), 5000); 48 | // }); 49 | // }; 50 | 51 | // const getURIListFromDatabase = (workspace_id) => { 52 | // fetch(`http://localhost:${process.env.PORT}/routes/${workspace_id}`) 53 | // .then((response) => response.json()) 54 | // .then((data) => { 55 | // setURIList(data); 56 | // }) 57 | // .catch((err) => { 58 | // setErrorMessage("Invalid db fetch request for the URI List"); 59 | // // * reset the error message 60 | // setTimeout(() => setErrorMessage(""), 5000); 61 | // }); 62 | // }; 63 | 64 | const handleStartMonitoringClick = (e) => { 65 | if (pingInterval === undefined || pingInterval < minPingInterval) return; 66 | e.preventDefault(); 67 | fetch(`http://localhost:${process.env.PORT}/monitoring`, { 68 | method: "POST", 69 | headers: { 70 | "Content-Type": "application/json" 71 | }, 72 | body: JSON.stringify({ 73 | active: true, 74 | domain, 75 | interval: pingInterval, 76 | metricsPort, 77 | mode: "monitoring", 78 | port, 79 | verbose: true, 80 | workspaceId 81 | }) 82 | }) 83 | .then((serverResponse) => { 84 | if (serverResponse.ok) { 85 | setIsMonitoring(true); 86 | } 87 | }) 88 | .catch((err) => { 89 | console.log("there was an error attempting to start monitoring: ", err); 90 | setErrorMessage( 91 | `Invalid POST request to start monitoring, error: ${err}` 92 | ); 93 | }); 94 | }; 95 | 96 | const handleStopMonitoringClick = (e) => { 97 | e.preventDefault(); 98 | setIsMonitoring(false); 99 | fetch(`http://localhost:${process.env.PORT}/monitoring`, { 100 | method: "POST", 101 | headers: { 102 | "Content-Type": "application/json" 103 | }, 104 | body: JSON.stringify({ 105 | active: false, 106 | verbose: true, 107 | workspaceId 108 | }) 109 | }) 110 | .then((serverResponse) => { 111 | if (serverResponse.ok) { 112 | setIsMonitoring(false); 113 | } 114 | }) 115 | .catch((err) => { 116 | console.log("there was an error attempting to stop monitoring: ", err); 117 | setErrorMessage( 118 | `Invalid POST request to stop monitoring, error: ${err}` 119 | ); 120 | }); 121 | }; 122 | 123 | return ( 124 | 127 | 128 | {name} 129 | 130 | 131 | {domain}{((port !== undefined && typeof port === "number") ? ':' + port : '')} 132 | 133 | Monitoring Frequency 134 | } 150 | endAdornment={s} 151 | fullWidth={true} 152 | size="lg" 153 | sx={{ width: 70 }} 154 | onChange={(e) => { 155 | setPingInterval(e.target.value); 156 | }} 157 | /> 158 |

159 | 160 | 174 | 188 | 189 | {/* */} 190 |

191 |
192 | {errorMessage !== "" ? ( 193 | 194 | ) : null} 195 | {/*
196 | { 201 | setMetricsPort(e.target.value); 202 | }} 203 | > 204 | 212 |
*/} 213 | {/* 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | {URIList.filter((uriObject) => { 225 | if (searchInput === "") { 226 | return uriObject; 227 | } else if (uriObject.path.toLowerCase() == searchInput) { 228 | return uriObject.path; 229 | } 230 | }).map((element) => { 231 | return ( 232 | 241 | ); 242 | })} 243 | 244 |
TrackingPathMethodStatus CodeSimulate User Activity
*/} 245 |
246 |
247 | ); 248 | }; 249 | 250 | export default WorkspaceInfo; 251 | -------------------------------------------------------------------------------- /client/containers/App.jsx: -------------------------------------------------------------------------------- 1 | import { CssBaseline, ThemeProvider } from "@mui/material"; 2 | import React, { useEffect, useState } from "react"; 3 | import { ColorModeContext, useMode } from "../theme.js"; 4 | 5 | import Home from "../components/Home.jsx"; 6 | import WorkspaceCard from "../components/WorkspaceCard.jsx"; 7 | import WorkspaceView from "./WorkspaceView.jsx"; 8 | import Settings from "../components/Settings.jsx"; 9 | import URI from "../components/URI.jsx"; 10 | import WorkspaceInfo from "../components/WorkspaceInfo.jsx"; 11 | import Dashboard from "./Dashboard.jsx"; 12 | import SimulationView from "./SimulationView.jsx"; 13 | import DrawerContents from "./DrawerContents.jsx"; 14 | // import NavBar from "./NavBar.jsx"; 15 | import SideBar from "./NavBar.jsx"; 16 | import TopBar from "./TopBar.jsx"; 17 | import { HashRouter as Router, Route, Routes } from "react-router-dom"; 18 | 19 | import "../styles/globals.scss"; 20 | 21 | import { styled, useTheme } from "@mui/material/styles"; 22 | import { Menu, ChevronLeft, ChevronRight } from "@mui/icons-material"; 23 | import { 24 | Box, 25 | Divider, 26 | Drawer as MuiDrawer, 27 | AppBar as MuiAppBar, 28 | Toolbar, 29 | IconButton, 30 | List, 31 | Typography, 32 | ListItem, 33 | ListItemButton, 34 | ListItemIcon, 35 | ListItemText 36 | } from "@mui/material"; 37 | 38 | const drawerWidth = 270; 39 | const openedMixin = (theme) => ({ 40 | width: drawerWidth, 41 | transition: theme.transitions.create("width", { 42 | easing: theme.transitions.easing.sharp, 43 | duration: theme.transitions.duration.enteringScreen 44 | }), 45 | overflowX: "hidden" 46 | }); 47 | const closedMixin = (theme) => ({ 48 | transition: theme.transitions.create("width", { 49 | easing: theme.transitions.easing.sharp, 50 | duration: theme.transitions.duration.leavingScreen 51 | }), 52 | overflowX: "hidden", 53 | width: `calc(${theme.spacing(7)} + 1px)`, 54 | [theme.breakpoints.up("sm")]: { 55 | width: `calc(${theme.spacing(8)} + 1px)` 56 | } 57 | }); 58 | const DrawerSection = styled("div")(({ theme }) => ({ 59 | display: "flex", 60 | alignItems: "center", 61 | justifyContent: "flex-end", 62 | padding: theme.spacing(0, 1), 63 | // necessary for content to be below app bar 64 | ...theme.mixins.toolbar 65 | })); 66 | const AppBar = styled(MuiAppBar, { 67 | shouldForwardProp: (prop) => prop !== "open" 68 | })(({ theme, open }) => ({ 69 | zIndex: theme.zIndex.drawer + 1, 70 | transition: theme.transitions.create(["width", "margin"], { 71 | easing: theme.transitions.easing.sharp, 72 | duration: theme.transitions.duration.leavingScreen 73 | }), 74 | ...(open && { 75 | marginLeft: drawerWidth, 76 | width: `calc(100% - ${drawerWidth}px)`, 77 | transition: theme.transitions.create(["width", "margin"], { 78 | easing: theme.transitions.easing.sharp, 79 | duration: theme.transitions.duration.enteringScreen 80 | }) 81 | }) 82 | })); 83 | const Drawer = styled(MuiDrawer, { 84 | shouldForwardProp: (prop) => prop !== "open" 85 | })(({ theme, open }) => ({ 86 | width: drawerWidth, 87 | flexShrink: 0, 88 | whiteSpace: "nowrap", 89 | boxSizing: "border-box", 90 | ...(open && { 91 | ...openedMixin(theme), 92 | "& .MuiDrawer-paper": openedMixin(theme) 93 | }), 94 | ...(!open && { 95 | ...closedMixin(theme), 96 | "& .MuiDrawer-paper": closedMixin(theme) 97 | }) 98 | })); 99 | 100 | const App = () => { 101 | const [theme, colorMode] = useMode(); 102 | const [open, setOpen] = useState(false); 103 | const [showSettingsPopup, setShowSettingsPopup] = useState(false); 104 | const handleDrawerOpen = () => setOpen(true); 105 | const handleDrawerClose = () => setOpen(false); 106 | 107 | return ( 108 | 109 | 110 | 111 | 112 | 113 | 114 | {/* Top Toolbar */} 115 | 116 | 117 | 127 | 128 | 129 | 133 | 134 | 135 | {/* */} 136 | 137 | {/* Drawer */} 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 150 | 151 | {/* */} 152 | 153 | 154 | {/* Main components */} 155 | 156 | 157 |
158 |
159 |
160 | 161 | } /> 162 | {/* } /> */} 163 | } /> 164 | } /> 165 | } /> 166 | } /> 167 | } /> 168 | {/* } /> */} 169 | } 172 | /> 173 | 174 | 178 |
179 |
180 |
181 |
182 | 183 | 184 | 185 | 186 | ); 187 | }; 188 | 189 | export default App; 190 | -------------------------------------------------------------------------------- /client/containers/ChartsContainer.jsx: -------------------------------------------------------------------------------- 1 | import { Box, Card, ToggleButton, ToggleButtonGroup, useTheme } from "@mui/material"; 2 | import Grid from "@mui/material/Unstable_Grid2"; 3 | import React, { useEffect, useState } from "react"; 4 | import DonutChart from "../components/DonutChart.jsx"; 5 | import Histogram from "../components/Histogram.jsx"; 6 | import LineChart from "../components/LineChart.jsx"; 7 | import "../styles/Charts.scss"; 8 | import { tokens } from "../theme.js"; 9 | 10 | const { SERVER_URL } = process.env; 11 | 12 | const ChartsContainer = (props) => { 13 | const { 14 | workspaceId, 15 | name, 16 | domain, 17 | port, 18 | metricsPort, 19 | endpointId, 20 | method, 21 | path, 22 | isMonitoring 23 | } = props; 24 | const [chartsData, setChartsData] = useState({}); 25 | const [range, setRange] = useState("1m"); 26 | 27 | const handleRangeChange = (event, newRange) => newRange ? setRange(newRange) : setRange(range); 28 | 29 | const theme = useTheme(); 30 | const colors = tokens(theme.palette.mode); 31 | 32 | useEffect(() => { 33 | if (isMonitoring) { 34 | setInterval(() => { 35 | const encodedPath = path.replaceAll("/", "%2F"); 36 | fetch( 37 | `${SERVER_URL}/chartdata/?workspaceId=${workspaceId}&method=${method}&path=${encodedPath}`, 38 | { 39 | method: "GET", 40 | headers: { "Content-Encoding": "gzip" } 41 | } 42 | ) 43 | .then((response) => response.json()) 44 | .then((dataObj) => { 45 | setChartsData(dataObj); 46 | }) 47 | .catch((err) => { 48 | console.log( 49 | `there was an error in the charts container fetch request, error: ${err}` 50 | ); 51 | }); 52 | }, 2000); 53 | } 54 | }, []); 55 | 56 | useEffect(() => { 57 | fetch(`${SERVER_URL}/chartData/`, { 58 | method: "POST", 59 | headers: { "Content-Type": "application/json" }, 60 | body: JSON.stringify({ range }) 61 | }) 62 | }, [range]) 63 | 64 | const cardStyle = { 65 | display: "flex", 66 | flexDirection: "column", 67 | justifyContent: "center", 68 | height: "270px", 69 | width: "100%", 70 | borderRadius: 3, 71 | p: 3, 72 | backgroundColor: `${colors.secondary[100]}` 73 | }; 74 | 75 | const toggleButtonGroupStyle = { 76 | alignSelf: 'end', 77 | mb: 2, 78 | }; 79 | 80 | const toggleButtonStyle = { 81 | borderRadius: 2, 82 | px: 1.5, 83 | py: 0.375, 84 | } 85 | 86 | return ( 87 | <> 88 | 92 | 100 | {/* 30s */} 101 | 1m 102 | 5m 103 | 30m 104 | 105 | 106 | {/* {orderedGridItems} */} 107 | 108 | 109 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 128 | 129 | 130 | 131 | 132 | 135 | 136 | 137 | 138 | 139 | 140 | ); 141 | }; 142 | 143 | export default ChartsContainer; 144 | -------------------------------------------------------------------------------- /client/containers/Dashboard.jsx: -------------------------------------------------------------------------------- 1 | import { Box, Typography } from "@mui/material"; 2 | import React from "react"; 3 | import { useLocation } from "react-router-dom"; 4 | import ChartsContainer from "./ChartsContainer.jsx"; 5 | 6 | const Dashboard = (props) => { 7 | 8 | const { workspaceId, endpointId, name, domain, port, method, path, isMonitoring, setIsMonitoring } = useLocation().state; 9 | 10 | return ( 11 | <> 12 | 19 | { 28 | switch (method) { 29 | case "GET": return "green" 30 | case "POST": return "yellow" 31 | case "PUT": return "blue" 32 | case "PATCH": return "grey" 33 | case "DELETE": return "red" 34 | default: return "grey" 35 | } 36 | })(method)), 37 | borderRadius: 1.5, 38 | px: 1, 39 | py: 0.25, 40 | }} 41 | > 42 | 43 | {method} 44 | 45 | 46 | 52 | {`http://${domain}${typeof port === "number" ? ':' + port : ''}${path}`} 53 | 54 | 55 | 65 | {/* */} 70 | 71 | ); 72 | }; 73 | 74 | export default Dashboard; 75 | -------------------------------------------------------------------------------- /client/containers/DrawerContents.jsx: -------------------------------------------------------------------------------- 1 | import { Home, Settings as SettingsIcon } from "@mui/icons-material"; 2 | import { 3 | Avatar, 4 | Divider, List, 5 | ListItem, 6 | ListItemButton, ListItemText 7 | } from "@mui/material"; 8 | import React, { useEffect, useState } from "react"; 9 | import { useNavigate } from "react-router-dom"; 10 | import Settings from "../components/Settings.jsx"; 11 | 12 | const DrawerContents = (props) => { 13 | const { 14 | open, 15 | showsettingspopup: showSettingsPopup, 16 | setshowsettingspopup: setShowSettingsPopup 17 | } = props; 18 | const [workspaceList, setWorkspaceList] = useState([]); 19 | const navigate = useNavigate(); 20 | 21 | useEffect(() => { 22 | getWorkSpaceList(); 23 | return; 24 | }, [open]); 25 | 26 | const getWorkSpaceList = async () => { 27 | const newWorkspaceList = await ( 28 | await fetch(`http://localhost:${process.env.PORT}/workspaces`) 29 | ).json() || []; 30 | setWorkspaceList(newWorkspaceList); 31 | return; 32 | }; 33 | 34 | return ( 35 | 36 | 37 | navigate("/")}> 38 | 46 | 47 | 48 | 53 | 54 | 55 | 56 | {workspaceList.map((workspace) => { 57 | return ( 58 | 63 | { 70 | navigate(`/workspace/${workspace._id}`, { 71 | state: { 72 | workspaceId: workspace._id, 73 | name: workspace.name, 74 | domain: workspace.domain, 75 | port: workspace.port, 76 | metricsPort: workspace.metrics_port 77 | } 78 | }); 79 | }} 80 | > 81 | 90 | {workspace.name 91 | .split(" ") 92 | .slice(0, 2) 93 | .map((word) => word[0]) 94 | .join("") 95 | .toUpperCase()} 96 | 97 | 103 | 104 | 105 | ); 106 | })} 107 | 108 | { 110 | if (showSettingsPopup) setShowSettingsPopup(false); 111 | else setShowSettingsPopup(true); 112 | }} 113 | > 114 | 122 | 123 | 124 | 125 | 130 | 131 | 132 | 133 | ); 134 | }; 135 | 136 | export default DrawerContents; 137 | -------------------------------------------------------------------------------- /client/containers/Header.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import '../styles/Header.scss' 3 | 4 | //header to create links that will be used to navigate between routes 5 | const Header = (props) => { 6 | const { monitoring, simulation } = props 7 | return( 8 |
9 | 21 |
22 | ) 23 | } 24 | 25 | export default Header -------------------------------------------------------------------------------- /client/containers/NavBar.jsx: -------------------------------------------------------------------------------- 1 | // import React, { useState, useEffect } from "react"; 2 | // import "../styles/NavBar.scss"; 3 | // import HomeButton from "../components/HomeButton.jsx"; 4 | // import { Link, NavLink, useNavigate } from "react-router-dom"; 5 | // import Draggable from "react-draggable"; 6 | // import { useTheme } from "@mui/material"; 7 | // import { tokens } from "../theme.js"; 8 | 9 | // const NavBar = (props) => { 10 | // const theme = useTheme(); 11 | // const colors = tokens(theme.palette.mode); 12 | // const { setMainWidth, setMainOffset } = props; 13 | // const [workspaceList, setWorkspaceList] = useState([]); 14 | 15 | // const navigate = useNavigate(); 16 | 17 | // const width = 300; 18 | // const height = "100vh"; 19 | // const [xPosition, setX] = useState(-width); 20 | 21 | // const toggleMenu = () => { 22 | // if (xPosition < 0) { 23 | // getWorkSpaceList(); 24 | // setX(0); 25 | // setMainWidth(`calc(100vw - ${width}px)`) 26 | // setMainOffset(`${width}px`) 27 | // } else { 28 | // setX(-width); 29 | // setMainWidth("100vw") 30 | // setMainOffset(`0px`) 31 | // } 32 | // }; 33 | 34 | // useEffect(() => { 35 | // setX(-width); 36 | // }, []); 37 | 38 | // useEffect(() => { 39 | // getWorkSpaceList(); 40 | // }, []); 41 | 42 | // const getWorkSpaceList = () => { 43 | // fetch(`http://localhost:${process.env.PORT}/workspaces`) 44 | // .then((response) => response.json()) 45 | // .then((data) => { 46 | // setWorkspaceList(data); 47 | // }) 48 | // .catch((err) => { 49 | // console.log(`there was an error: ${err}`); 50 | // }); 51 | // }; 52 | 53 | // return ( 54 | // 55 | //
64 | // 65 | //
66 | // 73 | //
74 | //
75 | // 83 | //
84 | // {workspaceList.map((workspace) => { 85 | // const workspace_id = workspace._id; 86 | // const name = workspace.name; 87 | // const domain = workspace.domain; 88 | // return ( 89 | //
90 | //
{ 95 | // navigate(`/urilist/${workspace_id}`, { 96 | // state: { workspace_id, name, domain } 97 | // }); 98 | // toggleMenu(); 99 | // }} 100 | // > 101 | // {name} 102 | //
103 | //
104 | // ); 105 | // })} 106 | //
107 | //
108 | //
109 | // ); 110 | // }; 111 | 112 | // export default NavBar; 113 | 114 | import React from "react"; 115 | import "../styles/NavBar.scss"; 116 | 117 | import { CssBaseline, ThemeProvider } from "@mui/material"; 118 | import { ChevronLeft, Menu } from "@mui/icons-material"; 119 | import { 120 | Divider, 121 | Drawer as MuiDrawer, 122 | IconButton, 123 | List, 124 | ListItem, 125 | ListItemButton, 126 | ListItemIcon, 127 | ListItemText 128 | } from "@mui/material"; 129 | import { styled } from "@mui/material/styles"; 130 | import { useTheme } from '@mui/material/styles' 131 | import { useMode } from '../theme.js' 132 | 133 | const SideBar = (props) => { 134 | const {open, handledrawerclose: handleDrawerClose } = props; 135 | // const theme = useTheme(); 136 | const [theme] = useMode(); 137 | 138 | const drawerWidth = 300; 139 | const openedMixin = (theme) => ({ 140 | width: drawerWidth, 141 | transition: theme.transitions.create("width", { 142 | easing: theme.transitions.easing.sharp, 143 | duration: theme.transitions.duration.enteringScreen 144 | }), 145 | overflowX: "hidden" 146 | }); 147 | const closedMixin = (theme) => ({ 148 | transition: theme.transitions.create("width", { 149 | easing: theme.transitions.easing.sharp, 150 | duration: theme.transitions.duration.leavingScreen 151 | }), 152 | overflowX: "hidden", 153 | width: `calc(${theme.spacing(7)} + 1px)`, 154 | [theme.breakpoints.up("sm")]: { 155 | width: `calc(${theme.spacing(8)} + 1px)` 156 | } 157 | }); 158 | const DrawerSection = styled("div")(({ theme }) => ({ 159 | display: "flex", 160 | alignItems: "center", 161 | justifyContent: "flex-end", 162 | padding: theme.spacing(0, 1), 163 | // necessary for content to be below app bar 164 | ...theme.mixins.toolbar 165 | })); 166 | const Drawer = styled(MuiDrawer, { 167 | shouldForwardProp: (prop) => prop !== "open" 168 | })(({ theme, open }) => ({ 169 | width: drawerWidth, 170 | flexShrink: 0, 171 | whiteSpace: "nowrap", 172 | boxSizing: "border-box", 173 | ...(open && { 174 | ...openedMixin(theme), 175 | "& .MuiDrawer-paper": openedMixin(theme) 176 | }), 177 | ...(!open && { 178 | ...closedMixin(theme), 179 | "& .MuiDrawer-paper": closedMixin(theme) 180 | }) 181 | })); 182 | 183 | return ( 184 | // 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | {["Inbox", "Starred", "Send email", "Drafts"].map((text, index) => ( 194 | 195 | 202 | 209 | 210 | 211 | 212 | 213 | 214 | ))} 215 | 216 | 217 | 218 | {["All mail", "Trash", "Spam"].map((text, index) => ( 219 | 220 | 227 | 234 | 235 | 236 | 237 | 238 | 239 | ))} 240 | 241 | 242 | // 243 | 244 | ); 245 | }; 246 | 247 | export default SideBar; 248 | -------------------------------------------------------------------------------- /client/containers/Production.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | //import components here 4 | import URIList from '../components/URIList.jsx' 5 | import ChartsContainer from "./ChartsContainer.jsx"; 6 | import NavBar from "./NavBar.jsx"; 7 | 8 | const Production = () => { 9 | return ( 10 |
11 | 12 |
13 | ) 14 | } 15 | 16 | export default Production; -------------------------------------------------------------------------------- /client/containers/Sidebar.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { ProSidebar, Menu, MenuItem } from "react-pro-sidebar"; 3 | // import "react-pro-sidebar/dist/css/styles.css"; 4 | import { Box, IconButton, Typography, useTheme } from "@mui/material"; 5 | // import { Link } from "react-router-dom"; 6 | import { tokens } from "../theme.js"; 7 | import MenuOutlinedIcon from "@mui/icons-material/MenuOutlined"; 8 | // import HomeOutlinedIcon from "@mui/icons-material/HomeOutlined"; 9 | // import PeopleOutlinedIcon from "@mui/icons-material/PeopleOutlined"; 10 | // import ContactsOutlinedIcon from "@mui/icons-material/ContactsOutlined"; 11 | // import ReceiptOutlinedIcon from "@mui/icons-material/ReceiptOutlined"; 12 | // import PersonOutlinedIcon from "@mui/icons-material/PersonOutlined"; 13 | // import CalendarTodayOutlinedIcon from "@mui/icons-material/CalendarTodayOutlined"; 14 | // import HelpOutlineOutlinedIcon from "@mui/icons-material/HelpOutlineOutlined"; 15 | // import BarChartOutlinedIcon from "@mui/icons-material/BarChartOutlined"; 16 | // import PieChartOutlineOutlinedIcon from "@mui/icons-material/PieChartOutlineOutlined"; 17 | // import TimelineOutlinedIcon from "@mui/icons-material/TimelineOutlined"; 18 | // import MapOutlinedIcon from "@mui/icons-material/MapOutlined"; 19 | 20 | // const Item = ({ title, to, icon, selected, setSelected }) => { 21 | // const theme = useTheme(); 22 | // const colors = tokens(theme.palette.mode); 23 | // return ( 24 | // setSelected(title)} 30 | // icon={icon} 31 | // > 32 | // {title} 33 | // 34 | // 35 | // ); 36 | // }; 37 | 38 | const Sidebar = () => { 39 | const theme = useTheme(); 40 | const colors = tokens(theme.palette.mode); 41 | const [isCollapsed, setIsCollapsed] = useState(false); 42 | const [selected, setSelected] = useState("Dashboard"); 43 | return ( 44 | 63 | 64 | 65 | setIsCollapsed(!isCollapsed)} 67 | icon={isCollapsed ? : undefined} 68 | style={{ 69 | margin: "10px 0 20px 0", 70 | color: colors.grey[100], 71 | }} 72 | > 73 | {!isCollapsed && ( 74 | 80 | 81 | ADMINIS 82 | 83 | setIsCollapsed(!isCollapsed)}> 84 | 85 | 86 | 87 | )} 88 | 89 | 90 | {!isCollapsed && ( 91 | 92 | 93 | profile-user 100 | 101 | 102 | 108 | The Best CEO 109 | 110 | 111 | VP Fancy Admin 112 | 113 | 114 | 115 | )} 116 | 117 | 118 | 119 | 120 | ); 121 | }; 122 | 123 | export default Sidebar; 124 | -------------------------------------------------------------------------------- /client/containers/SimulationView.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { Box, Slider, Typography, Button, ButtonGroup } from "@mui/material"; 3 | import { useLocation } from "react-router-dom"; 4 | 5 | const SimulationView = (props) => { 6 | const [settings, setSettings] = useState({ 7 | RPS: 100, 8 | timeInterval: 1, 9 | setTime: 3 10 | }); 11 | 12 | const location = useLocation(); 13 | const { domain, port, path, method, metricsPort, workspaceId } = 14 | location.state; 15 | 16 | function valuetext(value) { 17 | return `${value}`; 18 | } 19 | 20 | function handleChange(e, updatedVal) { 21 | const updatedInputVal = { [updatedVal]: e.target.value }; 22 | const updatedState = { 23 | ...settings, 24 | ...updatedInputVal 25 | }; 26 | setSettings(updatedState); 27 | } 28 | 29 | function handleTesting(e) { 30 | e.preventDefault(); 31 | fetch(`http://localhost:${process.env.PORT}/simulation`, { 32 | method: "POST", 33 | headers: { 34 | "Content-Type": "application/json" 35 | }, 36 | body: JSON.stringify({ 37 | ...settings, 38 | domain, 39 | port, 40 | path, 41 | method, 42 | mode: "simulation", 43 | metricsPort, 44 | workspaceId, 45 | stop: false 46 | }) 47 | }) 48 | .then((res) => res.json()) 49 | .then((data) => { 50 | // console.log('THIS IS FROM THE RESPONSE', data) 51 | }) 52 | .catch((err) => { 53 | console.log( 54 | `there was an error sending the simulation METRICS, error: ${err}` 55 | ); 56 | }); 57 | } 58 | 59 | function handleStop(e) { 60 | e.preventDefault(); 61 | fetch(`http://localhost:${process.env.PORT}/simulation`, { 62 | method: "POST", 63 | headers: { 64 | "Content-Type": "application/json" 65 | }, 66 | body: JSON.stringify({ 67 | ...settings, 68 | path: path, 69 | stop: true, 70 | metricsPort, 71 | mode: "simulation", 72 | workspaceId, 73 | }) 74 | }) 75 | .then((res) => res.json()) 76 | .then((data) => { 77 | console.log("THIS IS FROM THE RESPONSE", data); 78 | }) 79 | .catch((err) => { 80 | console.log( 81 | `there was an error sending the simulation METRICS, error: ${err}` 82 | ); 83 | }); 84 | } 85 | 86 | return ( 87 |
88 | 89 | 90 | Simulate User Activity 91 | 92 | 93 | Requests Per Second: 94 | 95 | handleChange(e, "RPS")} 108 | /> 109 | 110 | Time Interval: 111 | 112 | handleChange(e, "timeInterval")} 125 | /> 126 | 127 | Time Elapsed, Minutes: 128 | 129 | handleChange(e, "setTime")} 142 | /> 143 | 144 | 151 | 152 | 161 | 171 | 172 | 173 | 174 |
175 | ); 176 | }; 177 | 178 | export default SimulationView; 179 | -------------------------------------------------------------------------------- /client/containers/Topbar.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useContext } from "react"; 2 | import { Box, IconButton, useTheme } from "@mui/material"; 3 | import { 4 | LightModeOutlined, 5 | DarkModeOutlined, 6 | Settings as SettingsIcon, 7 | SettingsOutlined, 8 | NotificationsOutlined, 9 | Help 10 | } from "@mui/icons-material"; 11 | import { ColorModeContext, tokens } from "../theme.js"; 12 | import { Back, Forward } from "../components/NavButtons.jsx"; 13 | import Settings from "../components/Settings.jsx"; 14 | 15 | // ! Delete when done 16 | import { useNavigate } from "react-router-dom"; 17 | 18 | const TopBar = (props) => { 19 | 20 | const { showSettingsPopup, setShowSettingsPopup } = props; 21 | 22 | const theme = useTheme(); 23 | const colors = tokens(theme.palette.mode); 24 | const colorMode = useContext(ColorModeContext); 25 | return ( 26 | 79 | ); 80 | }; 81 | 82 | export default TopBar; 83 | -------------------------------------------------------------------------------- /client/containers/WorkspaceView.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React, { useState, useEffect } from "react"; 3 | import { useLocation, useNavigate } from "react-router-dom"; 4 | import { IconButton } from "@mui/material"; 5 | import { ChevronRight, Launch } from "@mui/icons-material"; 6 | import WorkspaceInfo from "../components/WorkspaceInfo.jsx" 7 | import URITable from "../components/URITable.jsx"; 8 | 9 | const WorkspaceView = () => { 10 | const location = useLocation(); 11 | const { workspaceId, name, domain, port } = location.state; 12 | const [URIList, setURIList] = useState([]); 13 | const [isMonitoring, setIsMonitoring] = useState(); 14 | const [metricsPort, setMetricsPort] = useState(location.state.metricsPort); 15 | 16 | const navigate = useNavigate(); 17 | 18 | useEffect(() => { 19 | fetch(`${process.env.SERVER_URL}/monitoring/${workspaceId}`) 20 | .then((serverResponse) => { 21 | return serverResponse.json(); 22 | }) 23 | .then((responseJson) => { 24 | setIsMonitoring(responseJson); 25 | }) 26 | }) 27 | 28 | useEffect(() => { 29 | getURIListFromDatabase(workspaceId); 30 | }, [workspaceId]); 31 | 32 | const addURIListToDatabase = async (workspaceId, URIList = URIList) => { 33 | await fetch(`${process.env.SERVER_URL}/routes/${workspaceId}`, { 34 | method: 'POST', 35 | headers: { 36 | "Content-Type": "application/json" 37 | }, 38 | body: JSON.stringify(URIList), 39 | }) 40 | } 41 | 42 | const getURIListFromServer = (metricsPortArg) => { 43 | fetch( 44 | `http://localhost:${process.env.PORT}/routes/server?metricsPort=${metricsPortArg}` 45 | ) 46 | .then((response) => response.json()) 47 | .then((data) => { 48 | setURIList(data); 49 | }) 50 | .catch((err) => { 51 | setErrorMessage("Invalid server fetch request for the URI List"); 52 | // * reset the error message 53 | setTimeout(() => setErrorMessage(""), 5000); 54 | }); 55 | }; 56 | 57 | const getURIListFromDatabase = (workspaceId) => { 58 | fetch(`http://localhost:${process.env.PORT}/routes/${workspaceId}`) 59 | .then((response) => response.json()) 60 | .then((data) => { 61 | setURIList(data); 62 | }) 63 | .catch((err) => { 64 | setErrorMessage("Invalid db fetch request for the URI List"); 65 | // * reset the error message 66 | setTimeout(() => setErrorMessage(""), 5000); 67 | }); 68 | }; 69 | 70 | const deleteURIListFromDatabase = (workspaceId) => { 71 | fetch(`http://localhost:${process.env.PORT}/endpoints/${workspaceId}`, { 72 | method: 'DELETE', 73 | }) 74 | .catch((err) => { 75 | setErrorMessage("Invalid db DELETE request for the URI List"); 76 | // * reset the error message 77 | setTimeout(() => setErrorMessage(""), 5000); 78 | }); 79 | }; 80 | 81 | const updateTrackingInDatabaseById = async (updatedEndpoint) => { 82 | fetch(`http://localhost:9990/endpoints/${updatedEndpoint._id}`, { 83 | method: `PUT`, 84 | headers: { "Content-Type": "application/json" }, 85 | body: JSON.stringify(updatedEndpoint) 86 | }).then((serverResponse) => { 87 | if (serverResponse.ok) { 88 | const updatedURIList = URIList.map((URI) => { 89 | return URI._id === updatedEndpoint._id ? updatedEndpoint : URI; 90 | }); 91 | setURIList(updatedURIList); 92 | } 93 | }); 94 | return; 95 | }; 96 | 97 | // const updateTrackingInDatabaseByRoute = async (updatedEndpoint) => { 98 | // fetch(`http://localhost:9990/endpoints2`, { 99 | // method: `PUT`, 100 | // headers: { "Content-Type": "application/json" }, 101 | // body: JSON.stringify(updatedEndpoint) 102 | // }).then((serverResponse) => { 103 | // if (serverResponse.ok) { 104 | // getURIListFromDatabase(workspaceId) 105 | // } 106 | // }); 107 | // return; 108 | // }; 109 | 110 | const refreshURIList = async (workspaceId = workspaceId, metricsPort = metricsPort) => { 111 | const response = await fetch(`${process.env.SERVER_URL}/routes/server?workspaceId=${workspaceId}&metricsPort=${metricsPort}`, { 112 | method: "PUT", 113 | headers: { 114 | "Content-type": "application/json" 115 | }, 116 | body: JSON.stringify({ 117 | metricsPort, 118 | workspaceId, 119 | }) 120 | }) 121 | const data = await response.json(); 122 | setURIList(data); 123 | } 124 | 125 | return ( 126 | <> 127 | 138 | { 147 | return { 148 | _id: URI._id, // hidden column 149 | _tracking: URI.tracking, // hidden column 150 | path: URI.path, 151 | method: URI.method, 152 | status_code: URI.statusCode || "N/A", 153 | simulation: 154 | { 156 | navigate(`/simulation/${crypto.randomUUID()}`, { 157 | state: { 158 | workspaceId, 159 | domain, 160 | port, 161 | metricsPort, 162 | path: URI.path, 163 | method: URI.method, 164 | } 165 | }) 166 | }} 167 | > 168 | 171 | , 172 | open: 173 | { 175 | const url = `http://${domain}${typeof port === "number" ? ':' + port : ""}${URI.path}` 176 | window.open(url); 177 | }} 178 | > 179 | 182 | 183 | }; 184 | })} 185 | updateTrackingInDatabaseById={updateTrackingInDatabaseById} 186 | getURIListFromServer={getURIListFromServer} 187 | refreshURIList={refreshURIList} 188 | /> 189 | 190 | ); 191 | }; 192 | 193 | export default WorkspaceView; -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import App from "./containers/App.jsx"; 4 | import { HashRouter } from "react-router-dom"; 5 | 6 | const container = document.getElementById("root"); 7 | const root = createRoot(container); 8 | 9 | root.render( 10 | <> 11 | 12 | 13 | ); -------------------------------------------------------------------------------- /client/styles/AddWorkspace.scss: -------------------------------------------------------------------------------- 1 | .modal { 2 | position: fixed; 3 | left: 0; 4 | top: 0; 5 | right: 0; 6 | bottom: 0; 7 | background-color: rgba(128,128,128,0.8); 8 | display: flex; 9 | align-items: center; 10 | justify-content: center; 11 | } 12 | 13 | .modal-content { 14 | padding: 40px; 15 | width: 25vw; 16 | height: 50vh; 17 | background-color: white; 18 | display: flex; 19 | flex-direction: column; 20 | justify-content: space-evenly; 21 | border-radius: 20px; 22 | } 23 | 24 | .close-button { 25 | align-self: flex-end; 26 | width: 40px; 27 | border-radius: 20px; 28 | } 29 | 30 | .modal-header { 31 | color: black; 32 | } 33 | 34 | .workspaceName { 35 | height: 8%; 36 | border-radius: 10px; 37 | } 38 | 39 | .alert-status-codes { 40 | height: 100%; 41 | resize: none; 42 | border-radius: 10px; 43 | } 44 | 45 | .monitoring-frequency{ 46 | height: 40%; 47 | resize: none; 48 | border-radius: 10px; 49 | } 50 | 51 | .submit-button { 52 | height: 8%; 53 | border-radius: 10px; 54 | } -------------------------------------------------------------------------------- /client/styles/Charts.scss: -------------------------------------------------------------------------------- 1 | // .charts-container { 2 | // display: grid; 3 | // width: 100vw; 4 | // grid-template-areas: 5 | // "3fr 2fr" 6 | // "3fr 2fr"; 7 | // gap: 2px; 8 | // // box-sizing: border-box; 9 | // } 10 | 11 | // // .chartWrapper { 12 | // // position: relative; 13 | // // } 14 | 15 | // .chartWrapper > canvas { 16 | // position: absolute; 17 | // left: 0; 18 | // top: 0; 19 | // pointer-events: none; 20 | // // width: 100%; 21 | // } 22 | 23 | // .chartAreaWrapper { 24 | // width: 60vw; 25 | // overflow-x: scroll; 26 | // } 27 | 28 | // .line-chart { 29 | // width: 80vw; 30 | // height: 40vh; 31 | // > canvas { 32 | // cursor: crosshair; 33 | // } 34 | // } 35 | -------------------------------------------------------------------------------- /client/styles/Header.scss: -------------------------------------------------------------------------------- 1 | .header-button{ 2 | opacity: 0.5; 3 | width: 100px; 4 | height: 30px; 5 | border-radius: 3px; 6 | cursor: auto; 7 | // text-align: right; 8 | float: right; 9 | } -------------------------------------------------------------------------------- /client/styles/NavBar.scss: -------------------------------------------------------------------------------- 1 | .navbar-container{ 2 | height: 100% !important; 3 | display: flex; 4 | flex-direction: column; 5 | // border-right: 1px solid; 6 | border-radius: 0; 7 | // border-color: rgba(132, 134, 133, 0.693); 8 | // background-color: rgb(255, 255, 255); 9 | transition: 0.5s ease; 10 | position: fixed; 11 | } 12 | 13 | .navbar-button{ 14 | height: 50px; 15 | border-top-right-radius: 10rem; 16 | border-bottom-right-radius: 9rem; 17 | width: 10px; 18 | position: absolute; 19 | outline: none; 20 | z-index: 1; 21 | background-color: rgb(197, 181, 181); 22 | border-color: rgba(174, 200, 188, 0.693); 23 | border-left: 0; 24 | cursor: pointer; 25 | } 26 | -------------------------------------------------------------------------------- /client/styles/Settings.scss: -------------------------------------------------------------------------------- 1 | .modal { 2 | position: fixed; 3 | left: 0; 4 | top: 0; 5 | right: 0; 6 | bottom: 0; 7 | background-color: rgba(128,128,128,0.8); 8 | display: flex; 9 | align-items: center; 10 | justify-content: center; 11 | } 12 | 13 | .modal-content { 14 | padding: 40px; 15 | width: 25vw; 16 | height: 50vh; 17 | background-color: white; 18 | display: flex; 19 | flex-direction: column; 20 | justify-content: space-evenly; 21 | border-radius: 20px; 22 | } 23 | 24 | .close-button { 25 | align-self: flex-end; 26 | width: 40px; 27 | border-radius: 20px; 28 | } 29 | 30 | .modal-header { 31 | color: black; 32 | } 33 | 34 | .workspaceDomain { 35 | height: 8%; 36 | border-radius: 10px; 37 | } 38 | 39 | .workspacePort { 40 | height: 8%; 41 | border-radius: 10px; 42 | } 43 | 44 | .submit-button { 45 | height: 8%; 46 | border-radius: 10px; 47 | } -------------------------------------------------------------------------------- /client/styles/WorkspaceBox.scss: -------------------------------------------------------------------------------- 1 | // .workspaceBox{ 2 | // padding: 5px; 3 | // width: 30%; 4 | // margin: 5px 5px 13px 10px; 5 | // float: left; 6 | // font-size: .8em; 7 | // box-shadow: 5px 5px 15px rgba(0, 0, 0, .2); 8 | // color: brown 9 | // } -------------------------------------------------------------------------------- /client/styles/globals.scss: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600;700&display=swap"); 2 | 3 | // * { 4 | // transition: background 0.2s, color 0.2s; 5 | // } 6 | 7 | body { 8 | top: 0; 9 | left: 0; 10 | margin: 0; 11 | width: 100vw; 12 | box-sizing: border-box; 13 | -moz-box-sizing: border-box; 14 | -webkit-box-sizing: border-box; 15 | } 16 | 17 | .content { 18 | box-sizing: border-box; 19 | -moz-box-sizing: border-box; 20 | -webkit-box-sizing: border-box; 21 | } 22 | 23 | .fullapp, 24 | .content { 25 | height: 100%; 26 | width: 100%; 27 | font-family: "Source Sans Pro", sans-serif; 28 | } 29 | 30 | .fullapp { 31 | display: flex; 32 | position: relative; 33 | } 34 | 35 | * { 36 | -moz-box-sizing: border-box; 37 | -webkit-box-sizing: border-box; 38 | box-sizing: border-box; 39 | } 40 | -------------------------------------------------------------------------------- /client/theme.js: -------------------------------------------------------------------------------- 1 | import { createContext, useState, useMemo } from "react"; 2 | import { createTheme } from "@mui/material/styles"; 3 | 4 | export const tokens = (mode) => ({ 5 | ...(mode === "dark" 6 | ? { 7 | grey: { 8 | 100: "#e0e0e0", 9 | 200: "#c2c2c2", 10 | 300: "#a3a3a3", 11 | 400: "#858585", 12 | 500: "#666666", 13 | 600: "#525252", 14 | 700: "#3d3d3d", 15 | 800: "#292929", 16 | 900: "#141414", 17 | }, 18 | primary: { 19 | 100: "#d0d1d5", 20 | 200: "#a1a4ab", 21 | 300: "#727681", 22 | 400: "#1F2A40", 23 | 500: "#141b2d", 24 | 600: "#101624", 25 | 700: "#0c101b", 26 | 800: "#080b12", 27 | 900: "#040509", 28 | }, 29 | secondary: { 30 | 100: "#13294B", 31 | }, 32 | greenAccent: { 33 | 100: "#dbf5ee", 34 | 200: "#b7ebde", 35 | 300: "#94e2cd", 36 | 400: "#70d8bd", 37 | 500: "#4cceac", 38 | 600: "#3da58a", 39 | 700: "#2e7c67", 40 | 800: "#1e5245", 41 | 900: "#0f2922", 42 | }, 43 | redAccent: { 44 | 100: "#f8dcdb", 45 | 200: "#f1b9b7", 46 | 300: "#e99592", 47 | 400: "#e2726e", 48 | 500: "#db4f4a", 49 | 600: "#af3f3b", 50 | 700: "#832f2c", 51 | 800: "#58201e", 52 | 900: "#2c100f", 53 | }, 54 | blueAccent: { 55 | 100: "#e1e2fe", 56 | 200: "#c3c6fd", 57 | 300: "#a4a9fc", 58 | 400: "#868dfb", 59 | 500: "#6870fa", 60 | 600: "#535ac8", 61 | 700: "#3e4396", 62 | 800: "#2a2d64", 63 | 900: "#151632", 64 | }, 65 | } 66 | : { 67 | grey: { 68 | 100: "#141414", 69 | 200: "#292929", 70 | 300: "#3d3d3d", 71 | 400: "#525252", 72 | 500: "#666666", 73 | 600: "#858585", 74 | 700: "#a3a3a3", 75 | 800: "#c2c2c2", 76 | 900: "#e0e0e0", 77 | }, 78 | primary: { 79 | 100: "#040509", 80 | 200: "#080b12", 81 | 300: "#0c101b", 82 | 400: "#f2f0f0", 83 | 500: "#141b2d", 84 | 600: "#434957", 85 | 700: "#727681", 86 | 800: "#a1a4ab", 87 | 900: "#d0d1d5", 88 | }, 89 | secondary: { 90 | 100: "#FFFFFF", 91 | }, 92 | greenAccent: { 93 | 100: "#0f2922", 94 | 200: "#1e5245", 95 | 300: "#2e7c67", 96 | 400: "#3da58a", 97 | 500: "#4cceac", 98 | 600: "#70d8bd", 99 | 700: "#94e2cd", 100 | 800: "#b7ebde", 101 | 900: "#dbf5ee", 102 | }, 103 | redAccent: { 104 | 100: "#2c100f", 105 | 200: "#58201e", 106 | 300: "#832f2c", 107 | 400: "#af3f3b", 108 | 500: "#db4f4a", 109 | 600: "#e2726e", 110 | 700: "#e99592", 111 | 800: "#f1b9b7", 112 | 900: "#f8dcdb", 113 | }, 114 | blueAccent: { 115 | 100: "#151632", 116 | 200: "#2a2d64", 117 | 300: "#3e4396", 118 | 400: "#535ac8", 119 | 500: "#6870fa", 120 | 600: "#868dfb", 121 | 700: "#a4a9fc", 122 | 800: "#c3c6fd", 123 | 900: "#e1e2fe", 124 | }, 125 | }), 126 | }); 127 | 128 | // mui theme settings 129 | export const themeSettings = (mode) => { 130 | const colors = tokens(mode); 131 | return { 132 | breakpoints: { 133 | values: { 134 | xs: 0, //0 135 | sm: 600, //600 136 | md: 900, //900 137 | lg: 1200, //1200 138 | xl: 1500, //1536 139 | }, 140 | }, 141 | palette: { 142 | mode: mode, 143 | ...(mode === "dark" 144 | ? { 145 | // palette values for dark mode 146 | primary: { 147 | main: colors.primary[500], 148 | }, 149 | secondary: { 150 | main: colors.greenAccent[600], 151 | }, 152 | neutral: { 153 | dark: colors.grey[700], 154 | main: colors.grey[500], 155 | light: colors.grey[100], 156 | }, 157 | background: { 158 | default: colors.primary[500], 159 | }, 160 | customRed: { 161 | main: "rgb(255, 64, 64)" 162 | }, 163 | disabled: { 164 | main: colors.grey[700], 165 | }, 166 | } 167 | : { 168 | // palette values for light mode 169 | primary: { 170 | main: colors.primary[100], 171 | }, 172 | secondary: { 173 | main: colors.greenAccent[500], 174 | }, 175 | neutral: { 176 | dark: colors.grey[700], 177 | main: colors.grey[700], 178 | light: colors.grey[100], 179 | }, 180 | background: { 181 | default: "#fcfcfc", 182 | }, 183 | customRed: { 184 | main: "#FF0000" 185 | }, 186 | disabled: { 187 | main: colors.grey[800], 188 | }, 189 | }), 190 | }, 191 | typography: { 192 | fontFamily: ["Source Sans Pro", "sans-serif"].join(","), 193 | fontSize: 12, 194 | h1: { 195 | fontFamily: ["Source Sans Pro", "sans-serif"].join(","), 196 | fontSize: 40, 197 | }, 198 | h2: { 199 | fontFamily: ["Source Sans Pro", "sans-serif"].join(","), 200 | fontSize: 32, 201 | }, 202 | h3: { 203 | fontFamily: ["Source Sans Pro", "sans-serif"].join(","), 204 | fontSize: 24, 205 | }, 206 | h4: { 207 | fontFamily: ["Source Sans Pro", "sans-serif"].join(","), 208 | fontSize: 20, 209 | }, 210 | h5: { 211 | fontFamily: ["Source Sans Pro", "sans-serif"].join(","), 212 | fontSize: 16, 213 | }, 214 | h6: { 215 | fontFamily: ["Source Sans Pro", "sans-serif"].join(","), 216 | fontSize: 14, 217 | }, 218 | }, 219 | }; 220 | }; 221 | 222 | // context for color mode 223 | export const ColorModeContext = createContext({ 224 | toggleColorMode: () => {}, 225 | }); 226 | 227 | export const useMode = () => { 228 | const [mode, setMode] = useState("light"); 229 | 230 | const colorMode = useMemo( 231 | () => ({ 232 | toggleColorMode: () => 233 | setMode((prev) => (prev === "light" ? "dark" : "light")), 234 | }), 235 | [] 236 | ); 237 | 238 | const theme = useMemo(() => createTheme(themeSettings(mode)), [mode]); 239 | return [theme, colorMode]; 240 | }; 241 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | services: 3 | influxdb: 4 | image: influxdb:2.4 5 | ports: 6 | - '8086:8086' 7 | environment: 8 | - DOCKER_INFLUXDB_INIT_MODE=${DB_INFLUXDB_INIT_MODE} 9 | - DOCKER_INFLUXDB_INIT_USERNAME=${DB_INFLUXDB_INIT_USERNAME} 10 | - DOCKER_INFLUXDB_INIT_PASSWORD=${DB_INFLUXDB_INIT_PASSWORD} 11 | - DOCKER_INFLUXDB_INIT_ORG=${DB_INFLUXDB_INIT_ORG} 12 | - DOCKER_INFLUXDB_INIT_BUCKET=${DB_INFLUXDB_INIT_BUCKET} 13 | - DOCKER_INFLUXDB_INIT_RETENTION=${DB_INFLUXDB_INIT_RETENTION} 14 | - DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=${DB_INFLUXDB_INIT_ADMIN_TOKEN} 15 | 16 | postgres: 17 | image: postgres:14.1-alpine 18 | ports: 19 | - '5433:5432' 20 | restart: always 21 | environment: 22 | - POSTGRES_USER=postgres 23 | - POSTGRES_PASSWORD=postgres 24 | volumes: 25 | - db:/var/lib/postgresql/data 26 | - ./server/models/postgres-init.sql:/docker-entrypoint-initdb.d/init.sql 27 | volumes: 28 | db: 29 | driver: local -------------------------------------------------------------------------------- /dummy/.gcloudignore: -------------------------------------------------------------------------------- 1 | # This file specifies files that are *not* uploaded to Google Cloud 2 | # using gcloud. It follows the same syntax as .gitignore, with the addition of 3 | # "#!include" directives (which insert the entries of the given .gitignore-style 4 | # file at that point). 5 | # 6 | # For more information, run: 7 | # $ gcloud topic gcloudignore 8 | # 9 | .gcloudignore 10 | # If you would like to upload your .git directory, .gitignore file or files 11 | # from your .gitignore file, remove the corresponding line 12 | # below: 13 | .git 14 | .gitignore 15 | 16 | # Node.js dependencies: 17 | node_modules/ 18 | 19 | module/ -------------------------------------------------------------------------------- /dummy/api.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | 3 | router.get('/', (req, res) => { 4 | res.send(`you're using the api`); 5 | }); 6 | 7 | router.get('/:text', (req, res) => { 8 | res.send(req.params.text.split('').reverse().join('')); 9 | }); 10 | 11 | module.exports = router; -------------------------------------------------------------------------------- /dummy/app.yaml: -------------------------------------------------------------------------------- 1 | runtime: nodejs16 -------------------------------------------------------------------------------- /dummy/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/DataDoc/b3ee07ce10363eb11cbe0a770c0f7b336fd0c14c/dummy/assets/favicon.ico -------------------------------------------------------------------------------- /dummy/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | JONATHAN'S WEBSITE 8 | 30 | 31 | 32 |

Welcome to jonathan.org

33 | 34 |

35 | fast 36 | 🐆

37 |
38 | 39 |

40 | slow 41 | 🐌

42 |
43 | 44 |

45 | new link 46 |

47 |
48 | 49 |

50 | Use our API! 51 |

52 |
53 | 54 | -------------------------------------------------------------------------------- /dummy/module/index.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const responseTime = require("response-time"); 3 | const listAllEndpoints = require("express-list-endpoints"); 4 | 5 | /** 6 | * Start a new Express server that will store and serve a 7 | * list of logs and endpoints of the target server 8 | */ 9 | const app = express(); 10 | 11 | /** 12 | * Registed endpoints are stored in memory and can be exported 13 | * through the /endpoints endpoint 14 | */ 15 | let endpoints = []; 16 | 17 | /** 18 | * Logs are stored in memory until they are cleared by a request 19 | * made to the DELETE /metrics endpoint 20 | */ 21 | let logs = []; 22 | 23 | module.exports = { 24 | gatherMetrics: responseTime((req, res, time) => { 25 | if (req.url) { 26 | logs.push({ 27 | date_created: new Date(), 28 | path: req.route?.path, 29 | url: req.url, 30 | method: req.method, 31 | status_code: res.statusCode, 32 | response_time: Number(time.toFixed(3)), 33 | }); 34 | } 35 | }), 36 | 37 | registerEndpoint: (req, res, next) => { 38 | return next(); 39 | }, 40 | 41 | exportEndpoints: (app) => { 42 | const registeredEndpoints = listAllEndpoints(app).filter((endpoint) => 43 | endpoint.middlewares.includes("registerEndpoint") 44 | ); 45 | const formattedEndpoints = []; 46 | for (const unformattedEndpoint of registeredEndpoints) 47 | for (const method of unformattedEndpoint.methods) 48 | formattedEndpoints.push({ 49 | path: unformattedEndpoint.path, 50 | method: method, 51 | }); 52 | return (endpoints = formattedEndpoints); 53 | }, 54 | 55 | exportAllEndpoints: (app) => { 56 | const registeredEndpoints = listAllEndpoints(app); 57 | const formattedEndpoints = []; 58 | for (const unformattedEndpoint of registeredEndpoints) 59 | for (const method of unformattedEndpoint.methods) 60 | formattedEndpoints.push({ 61 | path: unformattedEndpoint.path, 62 | method: method, 63 | }); 64 | return (endpoints = formattedEndpoints); 65 | }, 66 | 67 | startMetricsServer: async function (PORT = 9991) { 68 | app.get("/metrics", (req, res) => { 69 | return res.status(200).json(logs); 70 | }); 71 | app.delete("/metrics", (req, res) => { 72 | res.status(200).json(logs); 73 | logs = []; 74 | return; 75 | }); 76 | app.get("/endpoints", (req, res) => { 77 | return res.json(endpoints); 78 | }); 79 | app.listen(PORT, () => { 80 | console.log(`Metrics server started on port ${PORT}`); 81 | }); 82 | }, 83 | }; 84 | -------------------------------------------------------------------------------- /dummy/module/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-endpoints-monitor", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "express-endpoints-monitor", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "express": "^4.18.2", 13 | "express-list-endpoints": "^6.0.0", 14 | "response-time": "^2.3.2" 15 | } 16 | }, 17 | "node_modules/accepts": { 18 | "version": "1.3.8", 19 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 20 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 21 | "dependencies": { 22 | "mime-types": "~2.1.34", 23 | "negotiator": "0.6.3" 24 | }, 25 | "engines": { 26 | "node": ">= 0.6" 27 | } 28 | }, 29 | "node_modules/array-flatten": { 30 | "version": "1.1.1", 31 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 32 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" 33 | }, 34 | "node_modules/body-parser": { 35 | "version": "1.20.1", 36 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", 37 | "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", 38 | "dependencies": { 39 | "bytes": "3.1.2", 40 | "content-type": "~1.0.4", 41 | "debug": "2.6.9", 42 | "depd": "2.0.0", 43 | "destroy": "1.2.0", 44 | "http-errors": "2.0.0", 45 | "iconv-lite": "0.4.24", 46 | "on-finished": "2.4.1", 47 | "qs": "6.11.0", 48 | "raw-body": "2.5.1", 49 | "type-is": "~1.6.18", 50 | "unpipe": "1.0.0" 51 | }, 52 | "engines": { 53 | "node": ">= 0.8", 54 | "npm": "1.2.8000 || >= 1.4.16" 55 | } 56 | }, 57 | "node_modules/bytes": { 58 | "version": "3.1.2", 59 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 60 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 61 | "engines": { 62 | "node": ">= 0.8" 63 | } 64 | }, 65 | "node_modules/call-bind": { 66 | "version": "1.0.2", 67 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", 68 | "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", 69 | "dependencies": { 70 | "function-bind": "^1.1.1", 71 | "get-intrinsic": "^1.0.2" 72 | }, 73 | "funding": { 74 | "url": "https://github.com/sponsors/ljharb" 75 | } 76 | }, 77 | "node_modules/content-disposition": { 78 | "version": "0.5.4", 79 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", 80 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 81 | "dependencies": { 82 | "safe-buffer": "5.2.1" 83 | }, 84 | "engines": { 85 | "node": ">= 0.6" 86 | } 87 | }, 88 | "node_modules/content-type": { 89 | "version": "1.0.4", 90 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 91 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", 92 | "engines": { 93 | "node": ">= 0.6" 94 | } 95 | }, 96 | "node_modules/cookie": { 97 | "version": "0.5.0", 98 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", 99 | "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", 100 | "engines": { 101 | "node": ">= 0.6" 102 | } 103 | }, 104 | "node_modules/cookie-signature": { 105 | "version": "1.0.6", 106 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 107 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" 108 | }, 109 | "node_modules/debug": { 110 | "version": "2.6.9", 111 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 112 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 113 | "dependencies": { 114 | "ms": "2.0.0" 115 | } 116 | }, 117 | "node_modules/depd": { 118 | "version": "2.0.0", 119 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 120 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 121 | "engines": { 122 | "node": ">= 0.8" 123 | } 124 | }, 125 | "node_modules/destroy": { 126 | "version": "1.2.0", 127 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", 128 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", 129 | "engines": { 130 | "node": ">= 0.8", 131 | "npm": "1.2.8000 || >= 1.4.16" 132 | } 133 | }, 134 | "node_modules/ee-first": { 135 | "version": "1.1.1", 136 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 137 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" 138 | }, 139 | "node_modules/encodeurl": { 140 | "version": "1.0.2", 141 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 142 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", 143 | "engines": { 144 | "node": ">= 0.8" 145 | } 146 | }, 147 | "node_modules/escape-html": { 148 | "version": "1.0.3", 149 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 150 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" 151 | }, 152 | "node_modules/etag": { 153 | "version": "1.8.1", 154 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 155 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", 156 | "engines": { 157 | "node": ">= 0.6" 158 | } 159 | }, 160 | "node_modules/express": { 161 | "version": "4.18.2", 162 | "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", 163 | "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", 164 | "dependencies": { 165 | "accepts": "~1.3.8", 166 | "array-flatten": "1.1.1", 167 | "body-parser": "1.20.1", 168 | "content-disposition": "0.5.4", 169 | "content-type": "~1.0.4", 170 | "cookie": "0.5.0", 171 | "cookie-signature": "1.0.6", 172 | "debug": "2.6.9", 173 | "depd": "2.0.0", 174 | "encodeurl": "~1.0.2", 175 | "escape-html": "~1.0.3", 176 | "etag": "~1.8.1", 177 | "finalhandler": "1.2.0", 178 | "fresh": "0.5.2", 179 | "http-errors": "2.0.0", 180 | "merge-descriptors": "1.0.1", 181 | "methods": "~1.1.2", 182 | "on-finished": "2.4.1", 183 | "parseurl": "~1.3.3", 184 | "path-to-regexp": "0.1.7", 185 | "proxy-addr": "~2.0.7", 186 | "qs": "6.11.0", 187 | "range-parser": "~1.2.1", 188 | "safe-buffer": "5.2.1", 189 | "send": "0.18.0", 190 | "serve-static": "1.15.0", 191 | "setprototypeof": "1.2.0", 192 | "statuses": "2.0.1", 193 | "type-is": "~1.6.18", 194 | "utils-merge": "1.0.1", 195 | "vary": "~1.1.2" 196 | }, 197 | "engines": { 198 | "node": ">= 0.10.0" 199 | } 200 | }, 201 | "node_modules/express-list-endpoints": { 202 | "version": "6.0.0", 203 | "resolved": "https://registry.npmjs.org/express-list-endpoints/-/express-list-endpoints-6.0.0.tgz", 204 | "integrity": "sha512-1I30bSVego+AU/eSsX/bV2xrOXW5tFhsuXZp7wZd9396bAAxH7KHaAwLXQYra0Aw33xA67HmNiceGf2SOvXaLg==", 205 | "engines": { 206 | "node": ">=10" 207 | } 208 | }, 209 | "node_modules/finalhandler": { 210 | "version": "1.2.0", 211 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", 212 | "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", 213 | "dependencies": { 214 | "debug": "2.6.9", 215 | "encodeurl": "~1.0.2", 216 | "escape-html": "~1.0.3", 217 | "on-finished": "2.4.1", 218 | "parseurl": "~1.3.3", 219 | "statuses": "2.0.1", 220 | "unpipe": "~1.0.0" 221 | }, 222 | "engines": { 223 | "node": ">= 0.8" 224 | } 225 | }, 226 | "node_modules/forwarded": { 227 | "version": "0.2.0", 228 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 229 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 230 | "engines": { 231 | "node": ">= 0.6" 232 | } 233 | }, 234 | "node_modules/fresh": { 235 | "version": "0.5.2", 236 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 237 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", 238 | "engines": { 239 | "node": ">= 0.6" 240 | } 241 | }, 242 | "node_modules/function-bind": { 243 | "version": "1.1.1", 244 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 245 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" 246 | }, 247 | "node_modules/get-intrinsic": { 248 | "version": "1.1.3", 249 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", 250 | "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", 251 | "dependencies": { 252 | "function-bind": "^1.1.1", 253 | "has": "^1.0.3", 254 | "has-symbols": "^1.0.3" 255 | }, 256 | "funding": { 257 | "url": "https://github.com/sponsors/ljharb" 258 | } 259 | }, 260 | "node_modules/has": { 261 | "version": "1.0.3", 262 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 263 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 264 | "dependencies": { 265 | "function-bind": "^1.1.1" 266 | }, 267 | "engines": { 268 | "node": ">= 0.4.0" 269 | } 270 | }, 271 | "node_modules/has-symbols": { 272 | "version": "1.0.3", 273 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 274 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", 275 | "engines": { 276 | "node": ">= 0.4" 277 | }, 278 | "funding": { 279 | "url": "https://github.com/sponsors/ljharb" 280 | } 281 | }, 282 | "node_modules/http-errors": { 283 | "version": "2.0.0", 284 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 285 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 286 | "dependencies": { 287 | "depd": "2.0.0", 288 | "inherits": "2.0.4", 289 | "setprototypeof": "1.2.0", 290 | "statuses": "2.0.1", 291 | "toidentifier": "1.0.1" 292 | }, 293 | "engines": { 294 | "node": ">= 0.8" 295 | } 296 | }, 297 | "node_modules/iconv-lite": { 298 | "version": "0.4.24", 299 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 300 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 301 | "dependencies": { 302 | "safer-buffer": ">= 2.1.2 < 3" 303 | }, 304 | "engines": { 305 | "node": ">=0.10.0" 306 | } 307 | }, 308 | "node_modules/inherits": { 309 | "version": "2.0.4", 310 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 311 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 312 | }, 313 | "node_modules/ipaddr.js": { 314 | "version": "1.9.1", 315 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 316 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 317 | "engines": { 318 | "node": ">= 0.10" 319 | } 320 | }, 321 | "node_modules/media-typer": { 322 | "version": "0.3.0", 323 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 324 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", 325 | "engines": { 326 | "node": ">= 0.6" 327 | } 328 | }, 329 | "node_modules/merge-descriptors": { 330 | "version": "1.0.1", 331 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 332 | "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" 333 | }, 334 | "node_modules/methods": { 335 | "version": "1.1.2", 336 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 337 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", 338 | "engines": { 339 | "node": ">= 0.6" 340 | } 341 | }, 342 | "node_modules/mime": { 343 | "version": "1.6.0", 344 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 345 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", 346 | "bin": { 347 | "mime": "cli.js" 348 | }, 349 | "engines": { 350 | "node": ">=4" 351 | } 352 | }, 353 | "node_modules/mime-db": { 354 | "version": "1.52.0", 355 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 356 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 357 | "engines": { 358 | "node": ">= 0.6" 359 | } 360 | }, 361 | "node_modules/mime-types": { 362 | "version": "2.1.35", 363 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 364 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 365 | "dependencies": { 366 | "mime-db": "1.52.0" 367 | }, 368 | "engines": { 369 | "node": ">= 0.6" 370 | } 371 | }, 372 | "node_modules/ms": { 373 | "version": "2.0.0", 374 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 375 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" 376 | }, 377 | "node_modules/negotiator": { 378 | "version": "0.6.3", 379 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 380 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", 381 | "engines": { 382 | "node": ">= 0.6" 383 | } 384 | }, 385 | "node_modules/object-inspect": { 386 | "version": "1.12.2", 387 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", 388 | "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", 389 | "funding": { 390 | "url": "https://github.com/sponsors/ljharb" 391 | } 392 | }, 393 | "node_modules/on-finished": { 394 | "version": "2.4.1", 395 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 396 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 397 | "dependencies": { 398 | "ee-first": "1.1.1" 399 | }, 400 | "engines": { 401 | "node": ">= 0.8" 402 | } 403 | }, 404 | "node_modules/on-headers": { 405 | "version": "1.0.2", 406 | "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", 407 | "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", 408 | "engines": { 409 | "node": ">= 0.8" 410 | } 411 | }, 412 | "node_modules/parseurl": { 413 | "version": "1.3.3", 414 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 415 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 416 | "engines": { 417 | "node": ">= 0.8" 418 | } 419 | }, 420 | "node_modules/path-to-regexp": { 421 | "version": "0.1.7", 422 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 423 | "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" 424 | }, 425 | "node_modules/proxy-addr": { 426 | "version": "2.0.7", 427 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 428 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 429 | "dependencies": { 430 | "forwarded": "0.2.0", 431 | "ipaddr.js": "1.9.1" 432 | }, 433 | "engines": { 434 | "node": ">= 0.10" 435 | } 436 | }, 437 | "node_modules/qs": { 438 | "version": "6.11.0", 439 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", 440 | "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", 441 | "dependencies": { 442 | "side-channel": "^1.0.4" 443 | }, 444 | "engines": { 445 | "node": ">=0.6" 446 | }, 447 | "funding": { 448 | "url": "https://github.com/sponsors/ljharb" 449 | } 450 | }, 451 | "node_modules/range-parser": { 452 | "version": "1.2.1", 453 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 454 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 455 | "engines": { 456 | "node": ">= 0.6" 457 | } 458 | }, 459 | "node_modules/raw-body": { 460 | "version": "2.5.1", 461 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", 462 | "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", 463 | "dependencies": { 464 | "bytes": "3.1.2", 465 | "http-errors": "2.0.0", 466 | "iconv-lite": "0.4.24", 467 | "unpipe": "1.0.0" 468 | }, 469 | "engines": { 470 | "node": ">= 0.8" 471 | } 472 | }, 473 | "node_modules/response-time": { 474 | "version": "2.3.2", 475 | "resolved": "https://registry.npmjs.org/response-time/-/response-time-2.3.2.tgz", 476 | "integrity": "sha512-MUIDaDQf+CVqflfTdQ5yam+aYCkXj1PY8fjlPDQ6ppxJlmgZb864pHtA750mayywNg8tx4rS7qH9JXd/OF+3gw==", 477 | "dependencies": { 478 | "depd": "~1.1.0", 479 | "on-headers": "~1.0.1" 480 | }, 481 | "engines": { 482 | "node": ">= 0.8.0" 483 | } 484 | }, 485 | "node_modules/response-time/node_modules/depd": { 486 | "version": "1.1.2", 487 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 488 | "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", 489 | "engines": { 490 | "node": ">= 0.6" 491 | } 492 | }, 493 | "node_modules/safe-buffer": { 494 | "version": "5.2.1", 495 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 496 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 497 | "funding": [ 498 | { 499 | "type": "github", 500 | "url": "https://github.com/sponsors/feross" 501 | }, 502 | { 503 | "type": "patreon", 504 | "url": "https://www.patreon.com/feross" 505 | }, 506 | { 507 | "type": "consulting", 508 | "url": "https://feross.org/support" 509 | } 510 | ] 511 | }, 512 | "node_modules/safer-buffer": { 513 | "version": "2.1.2", 514 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 515 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 516 | }, 517 | "node_modules/send": { 518 | "version": "0.18.0", 519 | "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", 520 | "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", 521 | "dependencies": { 522 | "debug": "2.6.9", 523 | "depd": "2.0.0", 524 | "destroy": "1.2.0", 525 | "encodeurl": "~1.0.2", 526 | "escape-html": "~1.0.3", 527 | "etag": "~1.8.1", 528 | "fresh": "0.5.2", 529 | "http-errors": "2.0.0", 530 | "mime": "1.6.0", 531 | "ms": "2.1.3", 532 | "on-finished": "2.4.1", 533 | "range-parser": "~1.2.1", 534 | "statuses": "2.0.1" 535 | }, 536 | "engines": { 537 | "node": ">= 0.8.0" 538 | } 539 | }, 540 | "node_modules/send/node_modules/ms": { 541 | "version": "2.1.3", 542 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 543 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 544 | }, 545 | "node_modules/serve-static": { 546 | "version": "1.15.0", 547 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", 548 | "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", 549 | "dependencies": { 550 | "encodeurl": "~1.0.2", 551 | "escape-html": "~1.0.3", 552 | "parseurl": "~1.3.3", 553 | "send": "0.18.0" 554 | }, 555 | "engines": { 556 | "node": ">= 0.8.0" 557 | } 558 | }, 559 | "node_modules/setprototypeof": { 560 | "version": "1.2.0", 561 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 562 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 563 | }, 564 | "node_modules/side-channel": { 565 | "version": "1.0.4", 566 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", 567 | "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", 568 | "dependencies": { 569 | "call-bind": "^1.0.0", 570 | "get-intrinsic": "^1.0.2", 571 | "object-inspect": "^1.9.0" 572 | }, 573 | "funding": { 574 | "url": "https://github.com/sponsors/ljharb" 575 | } 576 | }, 577 | "node_modules/statuses": { 578 | "version": "2.0.1", 579 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 580 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 581 | "engines": { 582 | "node": ">= 0.8" 583 | } 584 | }, 585 | "node_modules/toidentifier": { 586 | "version": "1.0.1", 587 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 588 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 589 | "engines": { 590 | "node": ">=0.6" 591 | } 592 | }, 593 | "node_modules/type-is": { 594 | "version": "1.6.18", 595 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 596 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 597 | "dependencies": { 598 | "media-typer": "0.3.0", 599 | "mime-types": "~2.1.24" 600 | }, 601 | "engines": { 602 | "node": ">= 0.6" 603 | } 604 | }, 605 | "node_modules/unpipe": { 606 | "version": "1.0.0", 607 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 608 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 609 | "engines": { 610 | "node": ">= 0.8" 611 | } 612 | }, 613 | "node_modules/utils-merge": { 614 | "version": "1.0.1", 615 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 616 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", 617 | "engines": { 618 | "node": ">= 0.4.0" 619 | } 620 | }, 621 | "node_modules/vary": { 622 | "version": "1.1.2", 623 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 624 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", 625 | "engines": { 626 | "node": ">= 0.8" 627 | } 628 | } 629 | }, 630 | "dependencies": { 631 | "accepts": { 632 | "version": "1.3.8", 633 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 634 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 635 | "requires": { 636 | "mime-types": "~2.1.34", 637 | "negotiator": "0.6.3" 638 | } 639 | }, 640 | "array-flatten": { 641 | "version": "1.1.1", 642 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 643 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" 644 | }, 645 | "body-parser": { 646 | "version": "1.20.1", 647 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", 648 | "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", 649 | "requires": { 650 | "bytes": "3.1.2", 651 | "content-type": "~1.0.4", 652 | "debug": "2.6.9", 653 | "depd": "2.0.0", 654 | "destroy": "1.2.0", 655 | "http-errors": "2.0.0", 656 | "iconv-lite": "0.4.24", 657 | "on-finished": "2.4.1", 658 | "qs": "6.11.0", 659 | "raw-body": "2.5.1", 660 | "type-is": "~1.6.18", 661 | "unpipe": "1.0.0" 662 | } 663 | }, 664 | "bytes": { 665 | "version": "3.1.2", 666 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 667 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" 668 | }, 669 | "call-bind": { 670 | "version": "1.0.2", 671 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", 672 | "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", 673 | "requires": { 674 | "function-bind": "^1.1.1", 675 | "get-intrinsic": "^1.0.2" 676 | } 677 | }, 678 | "content-disposition": { 679 | "version": "0.5.4", 680 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", 681 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 682 | "requires": { 683 | "safe-buffer": "5.2.1" 684 | } 685 | }, 686 | "content-type": { 687 | "version": "1.0.4", 688 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 689 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 690 | }, 691 | "cookie": { 692 | "version": "0.5.0", 693 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", 694 | "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" 695 | }, 696 | "cookie-signature": { 697 | "version": "1.0.6", 698 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 699 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" 700 | }, 701 | "debug": { 702 | "version": "2.6.9", 703 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 704 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 705 | "requires": { 706 | "ms": "2.0.0" 707 | } 708 | }, 709 | "depd": { 710 | "version": "2.0.0", 711 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 712 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" 713 | }, 714 | "destroy": { 715 | "version": "1.2.0", 716 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", 717 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" 718 | }, 719 | "ee-first": { 720 | "version": "1.1.1", 721 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 722 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" 723 | }, 724 | "encodeurl": { 725 | "version": "1.0.2", 726 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 727 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" 728 | }, 729 | "escape-html": { 730 | "version": "1.0.3", 731 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 732 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" 733 | }, 734 | "etag": { 735 | "version": "1.8.1", 736 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 737 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" 738 | }, 739 | "express": { 740 | "version": "4.18.2", 741 | "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", 742 | "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", 743 | "requires": { 744 | "accepts": "~1.3.8", 745 | "array-flatten": "1.1.1", 746 | "body-parser": "1.20.1", 747 | "content-disposition": "0.5.4", 748 | "content-type": "~1.0.4", 749 | "cookie": "0.5.0", 750 | "cookie-signature": "1.0.6", 751 | "debug": "2.6.9", 752 | "depd": "2.0.0", 753 | "encodeurl": "~1.0.2", 754 | "escape-html": "~1.0.3", 755 | "etag": "~1.8.1", 756 | "finalhandler": "1.2.0", 757 | "fresh": "0.5.2", 758 | "http-errors": "2.0.0", 759 | "merge-descriptors": "1.0.1", 760 | "methods": "~1.1.2", 761 | "on-finished": "2.4.1", 762 | "parseurl": "~1.3.3", 763 | "path-to-regexp": "0.1.7", 764 | "proxy-addr": "~2.0.7", 765 | "qs": "6.11.0", 766 | "range-parser": "~1.2.1", 767 | "safe-buffer": "5.2.1", 768 | "send": "0.18.0", 769 | "serve-static": "1.15.0", 770 | "setprototypeof": "1.2.0", 771 | "statuses": "2.0.1", 772 | "type-is": "~1.6.18", 773 | "utils-merge": "1.0.1", 774 | "vary": "~1.1.2" 775 | } 776 | }, 777 | "express-list-endpoints": { 778 | "version": "6.0.0", 779 | "resolved": "https://registry.npmjs.org/express-list-endpoints/-/express-list-endpoints-6.0.0.tgz", 780 | "integrity": "sha512-1I30bSVego+AU/eSsX/bV2xrOXW5tFhsuXZp7wZd9396bAAxH7KHaAwLXQYra0Aw33xA67HmNiceGf2SOvXaLg==" 781 | }, 782 | "finalhandler": { 783 | "version": "1.2.0", 784 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", 785 | "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", 786 | "requires": { 787 | "debug": "2.6.9", 788 | "encodeurl": "~1.0.2", 789 | "escape-html": "~1.0.3", 790 | "on-finished": "2.4.1", 791 | "parseurl": "~1.3.3", 792 | "statuses": "2.0.1", 793 | "unpipe": "~1.0.0" 794 | } 795 | }, 796 | "forwarded": { 797 | "version": "0.2.0", 798 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 799 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" 800 | }, 801 | "fresh": { 802 | "version": "0.5.2", 803 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 804 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" 805 | }, 806 | "function-bind": { 807 | "version": "1.1.1", 808 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 809 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" 810 | }, 811 | "get-intrinsic": { 812 | "version": "1.1.3", 813 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", 814 | "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", 815 | "requires": { 816 | "function-bind": "^1.1.1", 817 | "has": "^1.0.3", 818 | "has-symbols": "^1.0.3" 819 | } 820 | }, 821 | "has": { 822 | "version": "1.0.3", 823 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 824 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 825 | "requires": { 826 | "function-bind": "^1.1.1" 827 | } 828 | }, 829 | "has-symbols": { 830 | "version": "1.0.3", 831 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 832 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" 833 | }, 834 | "http-errors": { 835 | "version": "2.0.0", 836 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 837 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 838 | "requires": { 839 | "depd": "2.0.0", 840 | "inherits": "2.0.4", 841 | "setprototypeof": "1.2.0", 842 | "statuses": "2.0.1", 843 | "toidentifier": "1.0.1" 844 | } 845 | }, 846 | "iconv-lite": { 847 | "version": "0.4.24", 848 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 849 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 850 | "requires": { 851 | "safer-buffer": ">= 2.1.2 < 3" 852 | } 853 | }, 854 | "inherits": { 855 | "version": "2.0.4", 856 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 857 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 858 | }, 859 | "ipaddr.js": { 860 | "version": "1.9.1", 861 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 862 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" 863 | }, 864 | "media-typer": { 865 | "version": "0.3.0", 866 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 867 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" 868 | }, 869 | "merge-descriptors": { 870 | "version": "1.0.1", 871 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 872 | "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" 873 | }, 874 | "methods": { 875 | "version": "1.1.2", 876 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 877 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" 878 | }, 879 | "mime": { 880 | "version": "1.6.0", 881 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 882 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" 883 | }, 884 | "mime-db": { 885 | "version": "1.52.0", 886 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 887 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" 888 | }, 889 | "mime-types": { 890 | "version": "2.1.35", 891 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 892 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 893 | "requires": { 894 | "mime-db": "1.52.0" 895 | } 896 | }, 897 | "ms": { 898 | "version": "2.0.0", 899 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 900 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" 901 | }, 902 | "negotiator": { 903 | "version": "0.6.3", 904 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 905 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" 906 | }, 907 | "object-inspect": { 908 | "version": "1.12.2", 909 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", 910 | "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" 911 | }, 912 | "on-finished": { 913 | "version": "2.4.1", 914 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 915 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 916 | "requires": { 917 | "ee-first": "1.1.1" 918 | } 919 | }, 920 | "on-headers": { 921 | "version": "1.0.2", 922 | "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", 923 | "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" 924 | }, 925 | "parseurl": { 926 | "version": "1.3.3", 927 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 928 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" 929 | }, 930 | "path-to-regexp": { 931 | "version": "0.1.7", 932 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 933 | "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" 934 | }, 935 | "proxy-addr": { 936 | "version": "2.0.7", 937 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 938 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 939 | "requires": { 940 | "forwarded": "0.2.0", 941 | "ipaddr.js": "1.9.1" 942 | } 943 | }, 944 | "qs": { 945 | "version": "6.11.0", 946 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", 947 | "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", 948 | "requires": { 949 | "side-channel": "^1.0.4" 950 | } 951 | }, 952 | "range-parser": { 953 | "version": "1.2.1", 954 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 955 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" 956 | }, 957 | "raw-body": { 958 | "version": "2.5.1", 959 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", 960 | "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", 961 | "requires": { 962 | "bytes": "3.1.2", 963 | "http-errors": "2.0.0", 964 | "iconv-lite": "0.4.24", 965 | "unpipe": "1.0.0" 966 | } 967 | }, 968 | "response-time": { 969 | "version": "2.3.2", 970 | "resolved": "https://registry.npmjs.org/response-time/-/response-time-2.3.2.tgz", 971 | "integrity": "sha512-MUIDaDQf+CVqflfTdQ5yam+aYCkXj1PY8fjlPDQ6ppxJlmgZb864pHtA750mayywNg8tx4rS7qH9JXd/OF+3gw==", 972 | "requires": { 973 | "depd": "~1.1.0", 974 | "on-headers": "~1.0.1" 975 | }, 976 | "dependencies": { 977 | "depd": { 978 | "version": "1.1.2", 979 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 980 | "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==" 981 | } 982 | } 983 | }, 984 | "safe-buffer": { 985 | "version": "5.2.1", 986 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 987 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" 988 | }, 989 | "safer-buffer": { 990 | "version": "2.1.2", 991 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 992 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 993 | }, 994 | "send": { 995 | "version": "0.18.0", 996 | "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", 997 | "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", 998 | "requires": { 999 | "debug": "2.6.9", 1000 | "depd": "2.0.0", 1001 | "destroy": "1.2.0", 1002 | "encodeurl": "~1.0.2", 1003 | "escape-html": "~1.0.3", 1004 | "etag": "~1.8.1", 1005 | "fresh": "0.5.2", 1006 | "http-errors": "2.0.0", 1007 | "mime": "1.6.0", 1008 | "ms": "2.1.3", 1009 | "on-finished": "2.4.1", 1010 | "range-parser": "~1.2.1", 1011 | "statuses": "2.0.1" 1012 | }, 1013 | "dependencies": { 1014 | "ms": { 1015 | "version": "2.1.3", 1016 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1017 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 1018 | } 1019 | } 1020 | }, 1021 | "serve-static": { 1022 | "version": "1.15.0", 1023 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", 1024 | "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", 1025 | "requires": { 1026 | "encodeurl": "~1.0.2", 1027 | "escape-html": "~1.0.3", 1028 | "parseurl": "~1.3.3", 1029 | "send": "0.18.0" 1030 | } 1031 | }, 1032 | "setprototypeof": { 1033 | "version": "1.2.0", 1034 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 1035 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 1036 | }, 1037 | "side-channel": { 1038 | "version": "1.0.4", 1039 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", 1040 | "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", 1041 | "requires": { 1042 | "call-bind": "^1.0.0", 1043 | "get-intrinsic": "^1.0.2", 1044 | "object-inspect": "^1.9.0" 1045 | } 1046 | }, 1047 | "statuses": { 1048 | "version": "2.0.1", 1049 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 1050 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" 1051 | }, 1052 | "toidentifier": { 1053 | "version": "1.0.1", 1054 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 1055 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" 1056 | }, 1057 | "type-is": { 1058 | "version": "1.6.18", 1059 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 1060 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 1061 | "requires": { 1062 | "media-typer": "0.3.0", 1063 | "mime-types": "~2.1.24" 1064 | } 1065 | }, 1066 | "unpipe": { 1067 | "version": "1.0.0", 1068 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 1069 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" 1070 | }, 1071 | "utils-merge": { 1072 | "version": "1.0.1", 1073 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 1074 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" 1075 | }, 1076 | "vary": { 1077 | "version": "1.1.2", 1078 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 1079 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" 1080 | } 1081 | } 1082 | } 1083 | -------------------------------------------------------------------------------- /dummy/module/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-endpoints-monitor", 3 | "version": "1.0.2", 4 | "description": "", 5 | "main": "index.js", 6 | "keywords": [], 7 | "author": "", 8 | "license": "ISC", 9 | "dependencies": { 10 | "express": "^4.18.2", 11 | "express-list-endpoints": "^6.0.0", 12 | "response-time": "^2.3.2" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /dummy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dummy", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node ." 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "express-endpoints-monitor": "^1.0.2", 14 | "nodemon": "^2.0.20" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /dummy/server.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const express = require("express"); 3 | const app = express(); 4 | const apiRouter = require("./api.js"); 5 | const ourModule = require("express-endpoints-monitor"); 6 | 7 | const PORT = 3000; 8 | const METRICS_PORT = 9991; 9 | 10 | app.use(express.json()); 11 | app.use(ourModule.gatherMetrics); 12 | 13 | app.use(express.static(path.join(__dirname, "assets"))); 14 | 15 | app.get("/", (req, res) => 16 | res.sendFile(path.resolve(__dirname, "./index.html")) 17 | ); 18 | 19 | app.use("/api", apiRouter); 20 | 21 | app.get("/fast", 22 | ourModule.registerEndpoint, 23 | (req, res) => { 24 | res.status(201).send("fast"); 25 | }); 26 | 27 | app.put("/fast", 28 | ourModule.registerEndpoint, 29 | (req, res) => { 30 | res.status(204).send("fast"); 31 | }); 32 | 33 | app.get("/slow", ourModule.registerEndpoint, (req, res) => { 34 | const validStatusCodes = [ 35 | 100, 102, 200, 200, 200, 202, 203, 204, 204, 210, 301, 302, 400, 401, 403, 404, 500, 505 36 | ]; 37 | 38 | const statusCode = 39 | validStatusCodes[Math.floor(Math.random() * validStatusCodes.length)]; 40 | const artificialDelay = Math.random() * 900; 41 | setTimeout(() => res.status(statusCode).send("slow"), artificialDelay); 42 | }); 43 | 44 | app.listen(PORT, () => { 45 | console.log(`Target server started on port ${PORT}`); 46 | 47 | // ourModule.exportEndpoints(app); 48 | ourModule.exportAllEndpoints(app); 49 | ourModule.startMetricsServer(METRICS_PORT); 50 | }); 51 | -------------------------------------------------------------------------------- /electron/electron-main.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | const { app, BrowserWindow } = require("electron"); 3 | const url = require("url"); 4 | 5 | const { SERVER_URL } = process.env; 6 | 7 | function createWindow() { 8 | let win = new BrowserWindow({ 9 | width: 960, 10 | height: 700, 11 | webPreferences: { 12 | nodeIntegration: true, 13 | worldSafeExecuteJavaScript: true, 14 | contextIsolation: true, 15 | }, 16 | }); 17 | if (process.env.NODE_ENV === 'production') { 18 | indexPath = url.format({ 19 | protocol: 'http:', 20 | host: 'localhost:9990', 21 | pathname: 'index.html', 22 | slashes: true 23 | }) 24 | } else { 25 | indexPath = url.format({ 26 | protocol: 'http:', 27 | host: 'localhost:8080', 28 | pathname: 'index.html', 29 | slashes: true 30 | }) 31 | } 32 | setTimeout(() => win.loadURL(indexPath), 1000); 33 | win.once('ready-to-show', () => { 34 | win.show() 35 | }) 36 | } 37 | 38 | app.whenReady().then(() => { 39 | createWindow(); 40 | app.on("activate", () => { 41 | if (BrowserWindow.getAllWindows().length === 0) createWindow(); 42 | }); 43 | }); 44 | 45 | app.on("window-all-closed", () => { 46 | if (process.platform !== "darwin") app.quit(); 47 | }); 48 | -------------------------------------------------------------------------------- /examples/endpoint_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/DataDoc/b3ee07ce10363eb11cbe0a770c0f7b336fd0c14c/examples/endpoint_view.png -------------------------------------------------------------------------------- /examples/home_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/DataDoc/b3ee07ce10363eb11cbe0a770c0f7b336fd0c14c/examples/home_view.png -------------------------------------------------------------------------------- /examples/intro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/DataDoc/b3ee07ce10363eb11cbe0a770c0f7b336fd0c14c/examples/intro.png -------------------------------------------------------------------------------- /examples/monitoring_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/DataDoc/b3ee07ce10363eb11cbe0a770c0f7b336fd0c14c/examples/monitoring_view.png -------------------------------------------------------------------------------- /examples/simulation_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/DataDoc/b3ee07ce10363eb11cbe0a770c0f7b336fd0c14c/examples/simulation_view.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "datadoc", 3 | "version": "1.0.0", 4 | "description": "", 5 | "productName": "DataDoc", 6 | "main": "electron/electron-main.js", 7 | "scripts": { 8 | "start": "NODE_ENV=production nodemon server/server.js & NODE_ENV=production electron .", 9 | "build": "NODE_ENV=production webpack", 10 | "dev": "clear & NODE_ENV=development webpack serve & NODE_ENV=development nodemon server/server.js & electron .", 11 | "dev:mock": "npm run dev & nodemon dummy/server.js", 12 | "test": "jest --verbose", 13 | "electron": "electron ." 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/oslabs-beta/DataDoc.git" 18 | }, 19 | "keywords": [], 20 | "author": "Jo Huang https://github.com/jochuang, Jonathan Huang https://github.com/JH51, Jamie Schiff https://github.com/jamieschiff, Mariam Zakariadze https://github.com/mariamzakariadze", 21 | "license": "ISC", 22 | "bugs": { 23 | "url": "https://github.com/oslabs-beta/DataDoc/issues" 24 | }, 25 | "homepage": "https://github.com/oslabs-beta/DataDoc#readme", 26 | "devDependencies": { 27 | "@babel/core": "^7.20.5", 28 | "@babel/preset-env": "^7.20.2", 29 | "@babel/preset-react": "^7.18.6", 30 | "babel-loader": "^9.1.0", 31 | "css-loader": "^6.7.2", 32 | "electron": "^22.0.0", 33 | "html-webpack-plugin": "^5.5.0", 34 | "jest": "^29.3.1", 35 | "react-router-dom": "^6.4.5", 36 | "sass-loader": "^13.2.0", 37 | "style-loader": "^3.3.1", 38 | "webpack": "^5.75.0", 39 | "webpack-cli": "^5.0.1", 40 | "webpack-dev-server": "^4.11.1", 41 | "webpack-hot-middleware": "^2.25.3" 42 | }, 43 | "dependencies": { 44 | "@emotion/react": "^11.10.5", 45 | "@emotion/styled": "^11.10.5", 46 | "@influxdata/influxdb-client": "^1.33.0", 47 | "@mui/icons-material": "^5.11.0", 48 | "@mui/material": "^5.11.1", 49 | "@sendgrid/mail": "^7.7.0", 50 | "chart.js": "^4.0.1", 51 | "chartjs-adapter-moment": "^1.0.1", 52 | "compression": "^1.7.4", 53 | "cors": "^2.8.5", 54 | "dotenv": "^16.0.3", 55 | "dotenv-webpack": "^8.0.1", 56 | "express": "^4.18.2", 57 | "express-endpoints-monitor": "^1.0.0", 58 | "node-fetch": "^2.6.7", 59 | "nodemon": "^2.0.20", 60 | "pg": "^8.8.0", 61 | "prom2json-se": "^0.6.0", 62 | "react": "^18.2.0", 63 | "react-chartjs-2": "^5.0.1", 64 | "react-dom": "^18.2.0", 65 | "react-draggable": "^4.4.5", 66 | "react-pro-sidebar": "^0.7.1", 67 | "react-router": "^6.4.5", 68 | "response-time": "^2.3.2", 69 | "sass": "^1.56.2", 70 | "twilio": "^3.84.0", 71 | "typescript": "^4.9.4", 72 | "uuidv4": "^6.2.13" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /server/controllers/influxController.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | const { InfluxDB } = require("@influxdata/influxdb-client"); 3 | const path = require("path"); 4 | const influxClient = require("../models/influx-client.js"); 5 | 6 | const token = process.env.DB_INFLUXDB_INIT_ADMIN_TOKEN; 7 | const org = process.env.DB_INFLUXDB_INIT_ORG; 8 | const bucket = process.env.DB_INFLUXDB_INIT_BUCKET; 9 | 10 | const queryApi = new InfluxDB({ 11 | url: "http://localhost:8086", 12 | token: token 13 | }).getQueryApi({ 14 | org, 15 | gzip: true, 16 | headers: { 17 | "Content-Encoding": "gzip" 18 | } 19 | }); 20 | 21 | const influxController = {}; 22 | 23 | let range = "1m"; 24 | 25 | // declare a data object to store chart data 26 | const data = { 27 | respTimeLineData: [], 28 | respTimeHistData: [], 29 | reqFreqLineData: [], 30 | statusPieData: [] 31 | }; 32 | 33 | influxController.updateRange = (req, res, next) => { 34 | range = req.body.range || range; 35 | console.log(`Updated range to: ${range}`) 36 | return next(); 37 | } 38 | 39 | influxController.getRespTimeLineData = (req, res, next) => { 40 | const fluxQuery = ` 41 | from(bucket: "dev-bucket") 42 | |> range(start: -${range}) 43 | |> filter(fn: (r) => r["_measurement"] == "monitoring${req.query?.workspaceId ? '_' + req.query.workspaceId : ''}") 44 | |> filter(fn: (r) => r["_field"] == "res_time") 45 | |> filter(fn: (r) => r["method"] == "${req.query.method}") 46 | |> filter(fn: (r) => r["path"] == "${req.query.path}") 47 | |> yield(name: "mean") 48 | `; 49 | 50 | // declare a metrics object to collect labels and data 51 | const metrics = []; 52 | 53 | queryApi.queryRows(fluxQuery, { 54 | next(row, tableMeta) { 55 | const o = tableMeta.toObject(row); 56 | metrics.push({ x: o._time, y: o._value }); 57 | }, 58 | error(error) { 59 | console.log("Query Finished ERROR"); 60 | return next(error); 61 | }, 62 | complete() { 63 | data.respTimeLineData = metrics; 64 | res.locals.data = data; 65 | // console.log("Query Finished SUCCESS"); 66 | return next(); 67 | } 68 | }); 69 | }; 70 | 71 | influxController.getRespTimeHistData = (req, res, next) => { 72 | const fluxQuery = ` 73 | from(bucket: "dev-bucket") 74 | |> range(start: -${range}) 75 | |> filter(fn: (r) => r["_measurement"] == "monitoring${req.query?.workspaceId ? '_' + req.query.workspaceId : ''}") 76 | |> filter(fn: (r) => r["_field"] == "res_time") 77 | |> filter(fn: (r) => r["method"] == "${req.query.method}") 78 | |> filter(fn: (r) => r["path"] == "${req.query.path}") 79 | |> histogram(bins: [0.1, 0.5, 1.0, 5.0, 10.0, 50.0, 100.0, 500.0, 1000.0, 5000.0]) 80 | `; 81 | 82 | // declare a metrics object to collect labels and data 83 | const metrics = []; 84 | 85 | // declare a variable 'le' (lower than or equal to) and 'respFreq' to collect labels and data 86 | const le = []; 87 | const respFreq = []; 88 | 89 | queryApi.queryRows(fluxQuery, { 90 | next(row, tableMeta) { 91 | const o = tableMeta.toObject(row); 92 | le.push(o.le); 93 | respFreq.push(o._value); 94 | }, 95 | error(error) { 96 | console.log("Query Finished ERROR"); 97 | return next(error); 98 | }, 99 | complete() { 100 | const newResFreq = respFreq.map((el, i) => { 101 | if (i === 0) return respFreq[i]; 102 | return respFreq[i] - respFreq[i - 1]; 103 | }); 104 | for (let i = 0; i < newResFreq.length; i++) { 105 | metrics.push({ x: le[i], y: newResFreq[i] }); 106 | } 107 | data.respTimeHistData = metrics; 108 | res.locals.data = data; 109 | // console.log("Query Finished SUCCESS"); 110 | return next(); 111 | } 112 | }); 113 | }; 114 | 115 | influxController.getReqFreqLineData = (req, res, next) => { 116 | const fluxQuery = ` 117 | from(bucket: "dev-bucket") 118 | |> range(start: -${range}) 119 | |> filter(fn: (r) => r["_measurement"] == "monitoring${req.query?.workspaceId ? '_' + req.query.workspaceId : ''}") 120 | |> filter(fn: (r) => r["_field"] == "res_time") 121 | |> filter(fn: (r) => r["method"] == "${req.query.method}") 122 | |> filter(fn: (r) => r["path"] == "${req.query.path}") 123 | |> aggregateWindow(every: ${(() => {switch (range) { 124 | case "1m": return "10s" 125 | case "5m": return "1m" 126 | case "30m": return "5m" 127 | default: return "10s" 128 | } 129 | })()}, fn: count, createEmpty: false) 130 | `; 131 | 132 | // declare a metrics object to collect labels and data 133 | const metrics = []; 134 | 135 | queryApi.queryRows(fluxQuery, { 136 | next(row, tableMeta) { 137 | const o = tableMeta.toObject(row); 138 | metrics.push({ x: o._time, y: o._value }); 139 | }, 140 | error(error) { 141 | console.log("Query Finished ERROR"); 142 | return next(error); 143 | }, 144 | complete() { 145 | data.reqFreqLineData = metrics; 146 | res.locals.data = data; 147 | // console.log("Query Finished SUCCESS"); 148 | return next(); 149 | } 150 | }); 151 | }; 152 | 153 | influxController.getStatusPieData = (req, res, next) => { 154 | const influxQuery = ` 155 | from(bucket: "dev-bucket") 156 | |> range(start: -${range}) 157 | |> filter(fn: (r) => r["_measurement"] == "monitoring${req.query?.workspaceId ? '_' + req.query.workspaceId : ''}") 158 | |> filter(fn: (r) => r["_field"] == "status_code") 159 | |> filter(fn: (r) => r["method"] == "${req.query.method}") 160 | |> filter(fn: (r) => r["path"] == "${req.query.path}") 161 | |> group(columns: ["_value"]) 162 | |> count(column: "_field") 163 | |> group() 164 | `; 165 | 166 | // declare a metrics object to collect labels and data 167 | const metrics = []; 168 | 169 | // declare a stats object to collect labels and data 170 | queryApi.queryRows(influxQuery, { 171 | next(row, tableMeta) { 172 | const o = tableMeta.toObject(row); 173 | metrics.push({ x: o._value, y: o._field }); 174 | }, 175 | error(error) { 176 | console.log("Query Finished ERROR"); 177 | return next(error); 178 | }, 179 | complete() { 180 | data.statusPieData = metrics; 181 | res.locals.data = data; 182 | // console.log("Query Finished SUCCESS"); 183 | return next(); 184 | } 185 | }); 186 | }; 187 | 188 | influxController.getEndpointLogs = (req, res, next) => { 189 | const influxQuery = ` 190 | from(bucket: "dev-bucket") 191 | |> range(start: -${range}) 192 | |> filter(fn: (r) => r["_measurement"] == "monitoring") 193 | |> filter(fn: (r) => r["_field"] == "res_time" or r["_field"] == "status_code") 194 | |> filter(fn: (r) => r["method"] == "${req.query.method}") 195 | |> filter(fn: (r) => r["path"] == "${req.query.path}") 196 | `; 197 | 198 | // declare a logs object to collect labels and data 199 | const logs = {}; 200 | 201 | // declare a stats object to collect labels and data 202 | queryApi.queryRows(influxQuery, { 203 | next(row, tableMeta) { 204 | const dataObject = tableMeta.toObject(row); 205 | if (logs[dataObject._time] === undefined) logs[dataObject._time] = {}; 206 | logs[dataObject._time].timestamp = dataObject._time; 207 | logs[dataObject._time][dataObject._field] = dataObject._value; 208 | }, 209 | error(error) { 210 | console.log("Query Finished ERROR"); 211 | return next(error); 212 | }, 213 | complete() { 214 | res.locals.logs = Object.values(logs); 215 | return next(); 216 | } 217 | }); 218 | }; 219 | 220 | module.exports = influxController; 221 | -------------------------------------------------------------------------------- /server/controllers/pgController.js: -------------------------------------------------------------------------------- 1 | const postgresClient = require("../models/postgres-client"); 2 | const influxController = require("./influxController"); 3 | 4 | const pgController = { 5 | 6 | deleteEndpointsByWorkspaceId: async (req, res, next) => { 7 | // const {workspaceId} = req.params; 8 | const {workspaceId} = res.locals; 9 | const queryText = ` 10 | DELETE 11 | FROM endpoints 12 | WHERE workspace_id=${workspaceId} 13 | ;`; 14 | try { 15 | await postgresClient.query(queryText); 16 | } catch (err) { 17 | return next(err); 18 | } 19 | return next(); 20 | }, 21 | 22 | updateEndpointById: async (req, res, next) => { 23 | const _id = Number(req.params?._id); 24 | const { method, path, tracking } = req.body; 25 | const queryText = ` 26 | UPDATE 27 | endpoints 28 | SET 29 | method='${method}', 30 | path='${path}', 31 | tracking=${tracking} 32 | WHERE 33 | _id=${_id} ; 34 | `; 35 | try { 36 | postgresClient.query(queryText); 37 | return next(); 38 | } catch (err) { 39 | return next(err); 40 | } 41 | }, 42 | updateEndpointByRoute: async (req, res, next) => { 43 | const { workspaceId, method, path, tracking} = req.body 44 | const queryText = ` 45 | UPDATE 46 | endpoints 47 | SET 48 | method='${method}', 49 | path='${path}', 50 | tracking=${tracking} 51 | WHERE 52 | workspace_id=${workspaceId} AND 53 | method='${method}' AND 54 | path='${path}' 55 | ;`; 56 | try { 57 | postgresClient.query(queryText); 58 | } catch (err) { 59 | return next(err); 60 | } 61 | return next(); 62 | } 63 | }; 64 | 65 | module.exports = pgController; 66 | -------------------------------------------------------------------------------- /server/models/influx-client.js: -------------------------------------------------------------------------------- 1 | const { InfluxDB, Point } = require("@influxdata/influxdb-client"); 2 | const dotenv = require("dotenv"); 3 | const path = require("path"); 4 | dotenv.config({ path: path.resolve(__dirname, "../../.env") }); 5 | 6 | const token = process.env.DB_INFLUXDB_INIT_ADMIN_TOKEN; 7 | const org = process.env.DB_INFLUXDB_INIT_ORG; 8 | const bucket = process.env.DB_INFLUXDB_INIT_BUCKET; 9 | 10 | const insertToDB = () => { 11 | // create a new instance of influxDB, providing URL and API token 12 | const client = new InfluxDB({ url: "http://localhost:8086", token: token }); 13 | // create a write client, providing influxDB organization and bucket name 14 | const writeApi = client.getWriteApi(org, bucket, "ns"); 15 | // create default tags to all points 16 | // writeApi.useDefaultTags({endpoint: '/signup'}) 17 | 18 | // use the point constructor passing in "measurement" (table) 19 | const point = new Point("metrics") 20 | .tag("path", "/good") 21 | .tag("method", "GET") 22 | .floatField("res_time", 60) 23 | .intField("status_code", 200); 24 | // .timestamp() 25 | 26 | writeApi.writePoint(point); 27 | 28 | writeApi.close().then(() => { 29 | console.log("WRITE FINISHED"); 30 | }); 31 | }; 32 | 33 | const insertMultiple = (pointsArr) => { 34 | try { 35 | const client = new InfluxDB({ 36 | url: "http://localhost:8086", 37 | token: token, 38 | options: { 39 | headers: { "Content-Encoding": "gzip" } 40 | } 41 | }); 42 | const writeApi = client.getWriteApi(org, bucket, "ms", { 43 | gzipThreshold: 0, 44 | headers: { "Content-Encoding": "gzip" } 45 | }); 46 | writeApi.writePoints(pointsArr); 47 | writeApi.close(); 48 | return true; 49 | } catch (e) { 50 | return false; 51 | } 52 | }; 53 | 54 | const insertRegistration = (point) => { 55 | const client = new InfluxDB({ url: "http://localhost:8086", token: token }); 56 | const writeApi = client.getWriteApi(org, bucket, "ns"); 57 | 58 | writeApi.writePoint(point); 59 | writeApi.close().then(() => { 60 | console.log("WRITE FINISHED"); 61 | }); 62 | }; 63 | 64 | module.exports = { insertToDB, insertMultiple, insertRegistration }; 65 | -------------------------------------------------------------------------------- /server/models/postgres-client.js: -------------------------------------------------------------------------------- 1 | const { Pool, Client } = require("pg"); 2 | const dotenv = require("dotenv"); 3 | dotenv.config(); 4 | 5 | const { PG_HOST, PG_PORT, PG_USER, PG_PASS, PG_DB } = process.env; 6 | 7 | const client = new Client({ 8 | host: PG_HOST, 9 | port: PG_PORT, 10 | user: PG_USER, 11 | password: PG_PASS, 12 | database: PG_DB, 13 | }) 14 | 15 | client.connect().then(() => { 16 | console.log("Connected to database"); 17 | client.query("SELECT NOW()"); 18 | }) 19 | 20 | module.exports = { 21 | query: (text, params, callback) => { 22 | return client.query(text, params, callback); 23 | }, 24 | }; -------------------------------------------------------------------------------- /server/models/postgres-init.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE workspaces ( 2 | _id SERIAL, 3 | name TEXT NOT NULL, 4 | domain TEXT NOT NULL, 5 | port INTEGER, 6 | metrics_port INTEGER 7 | ); 8 | 9 | CREATE TABLE endpoints ( 10 | _id SERIAL, 11 | method TEXT NOT NULL, 12 | path TEXT NOT NULL, 13 | tracking BOOLEAN DEFAULT false, 14 | workspace_id INTEGER NOT NULL 15 | ); 16 | 17 | ALTER TABLE endpoints 18 | ADD CONSTRAINT endpoints_uq 19 | UNIQUE (method, path, workspace_id); -------------------------------------------------------------------------------- /server/routes/chartdata.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const compression = require("compression"); 3 | const influxController = require("../controllers/influxController.js"); 4 | 5 | const router = express.Router(); 6 | router.use(compression()); 7 | 8 | // * Retrieve line chart data from InfluxDB 9 | router.get( 10 | "/", 11 | influxController.getRespTimeLineData, 12 | influxController.getRespTimeHistData, 13 | influxController.getReqFreqLineData, 14 | influxController.getStatusPieData, 15 | (req, res) => { 16 | return res.status(200).json(res.locals.data); 17 | } 18 | ); 19 | 20 | // * Update chart range 21 | router.post("/", 22 | influxController.updateRange, 23 | (req, res) => res.sendStatus(204) 24 | ) 25 | 26 | module.exports = router; 27 | -------------------------------------------------------------------------------- /server/routes/logRouter.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const compression = require("compression"); 3 | const influxController = require("../controllers/influxController.js"); 4 | 5 | const router = express.Router(); 6 | router.use(compression()); 7 | 8 | // get line chart data 9 | router.get( 10 | "/", 11 | influxController.getEndpointLogs, 12 | (req, res) => { 13 | return res.status(200).json(res.locals.logs); 14 | } 15 | ); 16 | 17 | module.exports = router; 18 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | require("dotenv").config({ path: path.resolve(__dirname, "../.env") }); 3 | const express = require("express"); 4 | const fetch = require("node-fetch"); 5 | const cors = require("cors"); 6 | const influxClient = require("./models/influx-client.js"); 7 | const { Point } = require("@influxdata/influxdb-client"); 8 | const chartRouter = require("./routes/chartdata"); 9 | const logRouter = require("./routes/logRouter.js"); 10 | const postgresClient = require("./models/postgres-client.js"); 11 | const { 12 | PhoneNumberContext 13 | } = require("twilio/lib/rest/lookups/v1/phoneNumber.js"); 14 | const pgController = require("./controllers/pgController.js"); 15 | const { query } = require("express"); 16 | 17 | const MODE = process.env.NODE_ENV || "production"; 18 | const PORT = process.env.PORT || 9990; 19 | 20 | const app = express(); 21 | app.use(express.json()); 22 | app.use(cors()); 23 | app.use(express.urlencoded({ extended: true })); 24 | 25 | if (MODE === "production") { 26 | app.use(express.static(path.join(__dirname, "../dist"))); 27 | } 28 | 29 | // * Route all /chartdata requests to chartRouter 30 | app.use("/chartdata", chartRouter); 31 | app.use("/logdata", logRouter); 32 | 33 | let intervalId; 34 | let logs = []; 35 | let selectedEndpoints = []; 36 | let activeWorkspaceId; 37 | let monitoringStartTime, monitoringEndTime, timeElapsed; 38 | const trackedWorkspaces = {}; 39 | 40 | const updateTimeElapsed = function () { 41 | monitoringEndTime = new Date(); 42 | timeElapsed = new Date(monitoringEndTime - monitoringStartTime); 43 | if (timeElapsed < 60 * 1000) return timeElapsed.getSeconds() + "s"; 44 | return timeElapsed.getMinutes() + "m" + (timeElapsed.getSeconds() % 60) + "s"; 45 | }; 46 | 47 | const timeSince = (startDate) => { 48 | const timeElapsed = new Date(new Date() - startDate) 49 | if (timeElapsed < 60 * 1000) return timeElapsed.getSeconds() + "s"; 50 | return timeElapsed.getMinutes() + "m" + (timeElapsed.getSeconds() % 60) + "s"; 51 | } 52 | 53 | const scrapeDataFromMetricsServer = async (metricsPort, tableName) => { 54 | try { 55 | logs = await ( 56 | await fetch(`http://localhost:${metricsPort}/metrics`, { 57 | method: "DELETE" 58 | }) 59 | ).json(); 60 | console.log(`Storing to ${logs.length} entries to ${tableName}`) 61 | storeLogsToDatabase(logs, tableName); 62 | return logs; 63 | } catch (e) { 64 | console.error(e); 65 | return []; 66 | } 67 | }; 68 | 69 | const storeLogsToDatabase = async (logsArr, tableName) => { 70 | try { 71 | const pointsArr = logsArr.map((log) => { 72 | return new Point(tableName) 73 | .tag("path", log.path) 74 | .tag("url", log.url) 75 | .tag("method", log.method) 76 | .floatField("res_time", log.response_time) 77 | .intField("status_code", log.status_code) 78 | .timestamp(new Date(log.date_created).getTime()); 79 | }); 80 | return influxClient.insertMultiple(pointsArr); 81 | } catch (e) { 82 | console.error(e); 83 | return false; 84 | } 85 | }; 86 | 87 | const getTrackedEndpointsByWorkspaceId = async (workspaceId) => { 88 | const queryText = ` 89 | SELECT * 90 | FROM endpoints 91 | WHERE 92 | workspace_id=${workspaceId} AND 93 | tracking=${true} 94 | ;`; 95 | const dbResponse = await postgresClient.query(queryText); 96 | return dbResponse.rows; 97 | } 98 | 99 | const pingEndpoints = async (domain, port, endpoints = []) => { 100 | const formattedPort = (port !== undefined && 0 < port && port < 9999) ? ':' + port : ''; 101 | for (const endpoint of endpoints) { 102 | try { 103 | await fetch(`http://${domain}${formattedPort}${endpoint.path}`, { 104 | method: endpoint.method, 105 | headers: { "Cache-Control": "no-store" } 106 | }); 107 | } catch (e) { 108 | console.error(e); 109 | } 110 | } 111 | }; 112 | 113 | // endpoint to register user email and status codes to database 114 | app.post( 115 | "/registration", 116 | (req, res, next) => { 117 | let { subscribers, status300, status400, status500 } = req.body; 118 | try { 119 | const point = new Point("registration") 120 | .tag("email", subscribers) 121 | .booleanField("300", status300) 122 | .booleanField("400", status400) 123 | .booleanField("500", status500); 124 | influxClient.insertRegistration(point); 125 | return next(); 126 | } catch (e) { 127 | console.error(e); 128 | } 129 | }, 130 | (req, res) => res.sendStatus(200) 131 | ); 132 | 133 | app.get("/monitoring/:workspaceId", 134 | (req, res) => { 135 | const {workspaceId} = req.params; 136 | return res.status(200).json(trackedWorkspaces[workspaceId]?.active || false) 137 | } 138 | ); 139 | 140 | app.post("/monitoring", async (req, res) => { 141 | // * active is a boolean, interval is in seconds 142 | const { active, domain, metricsPort, mode, port, verbose, workspaceId } = req.body; 143 | 144 | if (active) { 145 | // * Enforce a minimum interval 146 | let interval = Math.max(0.5, req.body.interval); 147 | if (trackedWorkspaces[workspaceId] === undefined) trackedWorkspaces[workspaceId] = {}; 148 | if (trackedWorkspaces[workspaceId].intervalId) clearInterval(intervalId); 149 | const start = new Date(); 150 | const endpoints = await getTrackedEndpointsByWorkspaceId(workspaceId) || []; 151 | trackedWorkspaces[workspaceId] = Object.assign(trackedWorkspaces[workspaceId] ? trackedWorkspaces[workspaceId] : {}, { 152 | active, 153 | interval, 154 | intervalId: setInterval(() => { 155 | const elapsed = timeSince(trackedWorkspaces[workspaceId].start || new Date()); 156 | trackedWorkspaces[workspaceId].elapsed = elapsed; 157 | if (verbose) { 158 | console.clear(); 159 | console.log(`Monitoring for ${elapsed}`); 160 | } 161 | pingEndpoints(domain, port || '', endpoints); 162 | scrapeDataFromMetricsServer(metricsPort || 9991, `${mode}_${workspaceId}`); 163 | }, interval * 1000), 164 | domain, 165 | endpoints, 166 | metricsPort, 167 | mode, 168 | port, 169 | start, 170 | end: null, 171 | elapsed: 0, 172 | }) 173 | } 174 | 175 | else { 176 | if (trackedWorkspaces[workspaceId]) { 177 | clearInterval(trackedWorkspaces[workspaceId]?.intervalId) 178 | trackedWorkspaces[workspaceId].active = false; 179 | } 180 | trackedWorkspaces[workspaceId] = Object.assign(trackedWorkspaces[workspaceId] ? trackedWorkspaces[workspaceId] : {}, { 181 | active, 182 | intervalId: null, 183 | endpoints: [], 184 | end: new Date(), 185 | }) 186 | }; 187 | 188 | if (verbose) { 189 | console.clear(); 190 | console.log(`ACTIVE: ${active}`); 191 | } 192 | 193 | res.sendStatus(204); 194 | }); 195 | 196 | const pingOneEndpoint = async (URI, method) => { 197 | console.log(`Sending traffic to: ${URI}`) 198 | try { 199 | await fetch(URI, { 200 | method: method, 201 | headers: { 202 | "Cache-Control": "no-cache" 203 | } 204 | }); 205 | } catch (e) { 206 | console.error(e); 207 | } 208 | }; 209 | 210 | const performRPS = async (domain, port, path, method, RPS) => { 211 | // console.log("PERFORMRPS"); 212 | // console.table({ 213 | // domain, 214 | // port, 215 | // path, 216 | // method, 217 | // RPS, 218 | // }) 219 | // console.log("PERFORMRPS URI\nhttp://" + domain + (typeof port === "number") ? port : '' + path); 220 | const interval = Math.floor(1000 / RPS); 221 | if (intervalId) clearInterval(intervalId); 222 | let counter = 0; 223 | intervalId = setInterval(() => { 224 | console.clear() 225 | console.log(++counter); 226 | pingOneEndpoint(`http://${domain}${(typeof port === "number") ? ':' + port : ''}${path}`, method); 227 | }, interval); 228 | }; 229 | 230 | const rpswithInterval = async (domain, port, path, method, RPS, timeInterval) => { 231 | if (intervalId) clearInterval(intervalId); 232 | intervalId = setInterval(() => { 233 | performRPS(domain, port, path, method, RPS); 234 | console.log("PING FINISHED"); 235 | }, timeInterval * 1000); 236 | }; 237 | 238 | app.post("/simulation", async (req, res) => { 239 | const { workspaceId, domain, port, path, method, metricsPort, RPS, timeInterval, setTime, stop } = req.body; 240 | if (!stop) { 241 | rpswithInterval(domain, port, path, method, RPS, timeInterval); 242 | scrapeDataFromMetricsServer(metricsPort, `simulation_${workspaceId}`); 243 | } else { 244 | clearInterval(intervalId) 245 | console.log("Scraping..."); 246 | scrapeDataFromMetricsServer(metricsPort, `simulation_${workspaceId}`) 247 | }; 248 | console.log("PING RESULT DONE"); 249 | return res.sendStatus(200); 250 | }); 251 | 252 | app.get("/metrics", async (req, res) => { 253 | return res.status(200).json(logs); 254 | }); 255 | 256 | app.put("/endpoints/:_id", 257 | pgController.updateEndpointById, 258 | (req, res) => { 259 | return res.sendStatus(204); 260 | } 261 | ) 262 | 263 | app.put("/endpoints2/", 264 | pgController.updateEndpointByRoute, 265 | (req, res) => { 266 | return res.sendStatus(204); 267 | } 268 | ) 269 | 270 | app.put("/routes/server", async (req, res, next) => { 271 | const { workspaceId, metricsPort } = req.body; 272 | try { 273 | res.locals.workspaceId = workspaceId; 274 | const response = await fetch(`http://localhost:${metricsPort}/endpoints`) 275 | const routes = await response.json(); 276 | let queryText = ` 277 | DELETE 278 | FROM endpoints 279 | WHERE workspace_id=${workspaceId} 280 | ;`; 281 | routes.forEach((route) => { 282 | // route.status = 200; 283 | route.tracking = false; 284 | queryText += ` 285 | INSERT INTO endpoints (method, path, tracking, workspace_id) 286 | VALUES ('${route.method}', '${route.path}', ${route.tracking}, ${workspaceId}) 287 | ON CONFLICT ON CONSTRAINT endpoints_uq 288 | DO UPDATE SET tracking = ${route.tracking}; 289 | `; 290 | }); 291 | await postgresClient.query(queryText); 292 | } 293 | catch (err) { 294 | return console.error(err); 295 | } 296 | let dbResponse = []; 297 | try { 298 | queryText = ` 299 | SELECT * 300 | FROM endpoints 301 | WHERE workspace_id=${workspaceId} 302 | ;`; 303 | dbResponse = (await postgresClient.query(queryText)).rows; 304 | } 305 | catch (err) { 306 | console.error(err); 307 | return next(err); 308 | } 309 | return res.status(200).json(dbResponse); 310 | }); 311 | 312 | app.get("/routes/:workspace_id", async (req, res) => { 313 | const { workspace_id } = req.params; 314 | const queryText = ` 315 | SELECT * 316 | FROM endpoints 317 | WHERE workspace_id = $1;`; 318 | const dbResponse = await postgresClient.query(queryText, [workspace_id]); 319 | return res.status(200).json(dbResponse.rows); 320 | }); 321 | 322 | app.post("/routes/:workspace_id", async (req, res) => { 323 | const { workspace_id } = req.params; 324 | let queryText = ""; 325 | req.body.forEach((URI) => { 326 | queryText += ` 327 | INSERT INTO endpoints (method, path, tracking, workspace_id) 328 | VALUES ('${URI.method}', '${URI.path}', ${URI.tracking}, ${workspace_id}) 329 | ON CONFLICT ON CONSTRAINT endpoints_uq 330 | DO UPDATE SET tracking = ${URI.tracking};`; 331 | }); 332 | postgresClient.query(queryText); 333 | selectedEndpoints = req.body.filter((URI) => URI.tracking) || req.body; 334 | return res.sendStatus(204); 335 | }); 336 | 337 | // get existing workspaces for the user 338 | app.get("/workspaces", async (req, res) => { 339 | const queryText = ` 340 | SELECT * 341 | FROM workspaces 342 | ;`; 343 | const dbResponse = await postgresClient.query(queryText); 344 | return res.status(200).json(dbResponse.rows); 345 | }); 346 | 347 | // create a new workspace for the user 348 | app.post("/workspaces", async (req, res) => { 349 | const { name, domain, port, metricsPort } = req.body; 350 | let queryText = ` 351 | INSERT INTO workspaces (name, domain, port, metrics_port) 352 | VALUES ($1, $2, $3, $4) 353 | ;`; 354 | postgresClient.query(queryText, [name, domain, port, metricsPort]); 355 | return res.sendStatus(204); 356 | }); 357 | 358 | app.delete("/workspaces", async (req, res) => { 359 | const { workspace_id } = req.body; 360 | const queryText = ` 361 | DELETE FROM workspaces 362 | WHERE _id=${workspace_id} 363 | ;`; 364 | await postgresClient.query(queryText); 365 | return res.sendStatus(204); 366 | }); 367 | 368 | app.delete("/endpoints/:workspaceId", 369 | pgController.deleteEndpointsByWorkspaceId, 370 | async (req, res) => { 371 | return res.sendStatus(204); 372 | } 373 | ); 374 | 375 | app.listen(PORT, () => { 376 | console.log( 377 | `Application server started on port ${PORT}\n${MODE.toUpperCase()} mode` 378 | ); 379 | }); 380 | -------------------------------------------------------------------------------- /server/twilio/email.js: -------------------------------------------------------------------------------- 1 | const dotenv = require('dotenv') 2 | const path = require('path'); 3 | dotenv.config({path: path.resolve(__dirname, "../../.env")}); 4 | 5 | const sendGridMail = require("@sendgrid/mail"); 6 | // console.log(process.env.SENDGRID_API_KEY) 7 | sendGridMail.setApiKey(process.env.SENDGRID_API_KEY); 8 | 9 | //function to build out the body of the email 10 | const msg = () => { 11 | const body = 12 | "This is a notifaction from the Datatective team. We have found an outage in your system, please open up the Datatective desktop application for more information."; 13 | return { 14 | to: "example@domain.com", // ! query from database 15 | from: "datadocteam@gmail.com", 16 | subject: "IMPORTANT: outage detected from datatective", 17 | text: body, 18 | html: "{body}", 19 | }; 20 | }; 21 | 22 | //send the message using the send method from the SendGrid email package 23 | async function sendEmail() { 24 | try { 25 | await sendGridMail.send(msg()); 26 | console.log("Email notification successfully send"); 27 | } catch (error) { 28 | console.log("There was an error sending an email notification"); 29 | console.log("THIS IS THE ERROR: ", error); 30 | console.log("this is the API key: ", process.env.SENDGRID_API_KEY); 31 | if (error.response) { 32 | console.log(error.response.body); 33 | } 34 | } 35 | } 36 | 37 | (async () => { 38 | console.log("Sending email"); 39 | await sendEmail(); 40 | })(); 41 | -------------------------------------------------------------------------------- /server/twilio/twilio.js: -------------------------------------------------------------------------------- 1 | const twilio = require("twilio"); 2 | -------------------------------------------------------------------------------- /template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | DataDoc 7 | 8 | 9 | 10 |
11 | 12 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 2 | const Dotenv = require("dotenv-webpack"); 3 | const path = require("path"); 4 | 5 | module.exports = { 6 | mode: process.env.NODE_ENV || "production", 7 | entry: path.resolve(__dirname, "/client/index.js"), 8 | output: { 9 | path: path.resolve(__dirname, "dist"), 10 | filename: "bundle.js", 11 | publicPath: "/", 12 | }, 13 | devtool: "source-map", 14 | devServer: { 15 | static: { 16 | directory: path.resolve(__dirname, "dist"), 17 | }, 18 | port: 8080, 19 | hot: true, 20 | compress: true, 21 | historyApiFallback: true, 22 | }, 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.jsx?/, 27 | exclude: /node_modules/, 28 | loader: "babel-loader", 29 | options: { presets: ["@babel/env", "@babel/preset-react"] }, 30 | }, 31 | { 32 | test: /.(css|scss)$/, 33 | exclude: /node_modules/, 34 | use: ["style-loader", "css-loader", "sass-loader"], 35 | }, 36 | ], 37 | }, 38 | plugins: [ 39 | new HtmlWebpackPlugin({ 40 | filename: "index.html", 41 | template: "./template.html", 42 | }), 43 | new Dotenv({ 44 | systemvars: true, 45 | }), 46 | ], 47 | }; 48 | --------------------------------------------------------------------------------