├── public ├── lua.png ├── logo.ico ├── logo.png ├── logo_512.png ├── github-mark.png ├── grafana-metrics.png ├── run-first-test.gif ├── run-multiple-tests.gif └── logo-removebg-preview.png ├── jsconfig.json ├── .dockerignore ├── postcss.config.js ├── next.config.js ├── .prettierrc.json ├── src ├── pages │ ├── _document.tsx │ ├── 404.tsx │ ├── _app.tsx │ ├── dashboard │ │ ├── types │ │ │ └── types.ts │ │ ├── layout.tsx │ │ ├── constants │ │ │ └── constants.ts │ │ ├── indexComponents │ │ │ ├── currentMethods.tsx │ │ │ ├── plotlyChart.tsx │ │ │ └── inputData.tsx │ │ ├── metrics.tsx │ │ └── index.tsx │ ├── api │ │ ├── createBash.ts │ │ ├── execScript.ts │ │ ├── panels.ts │ │ ├── createLua.ts │ │ ├── createHistogram.ts │ │ └── histogramCache.ts │ ├── layout.tsx │ ├── index.tsx │ └── about │ │ └── index.tsx ├── lib │ ├── grafanaUrls.json │ └── histogramCache.json └── styles │ └── globals.css ├── Dockerfile ├── methods └── getHistogramData.ts ├── tailwind.config.js ├── .gitignore ├── tsconfig.json ├── components ├── Chart │ └── Chart.tsx ├── Panel │ └── Panel.tsx ├── Sidebar │ └── Sidebar.tsx └── Header │ └── Header.tsx ├── package.json ├── LICENSE └── README.md /public/lua.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/strapi/HEAD/public/lua.png -------------------------------------------------------------------------------- /public/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/strapi/HEAD/public/logo.ico -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/strapi/HEAD/public/logo.png -------------------------------------------------------------------------------- /public/logo_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/strapi/HEAD/public/logo_512.png -------------------------------------------------------------------------------- /public/github-mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/strapi/HEAD/public/github-mark.png -------------------------------------------------------------------------------- /public/grafana-metrics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/strapi/HEAD/public/grafana-metrics.png -------------------------------------------------------------------------------- /public/run-first-test.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/strapi/HEAD/public/run-first-test.gif -------------------------------------------------------------------------------- /public/run-multiple-tests.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/strapi/HEAD/public/run-multiple-tests.gif -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "@/*": ["./src/*"] 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /public/logo-removebg-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/strapi/HEAD/public/logo-removebg-preview.png -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | #DO NOT USE: Docker implementation not available. Wrk2 not easily implementable 2 | node_modules 3 | .next -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: false, 4 | pageExtensions: ['jsx', 'js', 'tsx', 'ts'], 5 | }; 6 | 7 | module.exports = nextConfig; 8 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "bracketSpacing": true, 7 | "arrowParens": "always", 8 | "printWidth": 80 9 | } 10 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document'; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | {/* */} 7 | 8 | 9 |
10 | 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Layout from './layout'; 3 | 4 | const NotFound = () => { 5 | return ( 6 | 7 |
8 |

404

9 |

Page Not Found :(

10 |
11 |
12 | ); 13 | }; 14 | 15 | export default NotFound; 16 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '@/styles/globals.css'; 2 | import type { AppProps } from 'next/app'; 3 | 4 | export default function App({ Component, pageProps }: AppProps) { 5 | return ( 6 | <> 7 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/pages/dashboard/types/types.ts: -------------------------------------------------------------------------------- 1 | export type testingConstants = { 2 | rootUrl: string; 3 | numOfThreads: number; 4 | testDuration: number; 5 | numOfUsers: number; 6 | throughput: number; 7 | }; 8 | 9 | export type params = { 10 | route: string; 11 | method: 'GET' | 'POST'; 12 | body?: string; 13 | contentType: 'application/json'; 14 | ratio: number; 15 | }; 16 | 17 | export type methods = params[]; -------------------------------------------------------------------------------- /src/pages/dashboard/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Layout from '../layout'; 3 | import Sidebar from '../../../components/Sidebar/Sidebar'; 4 | 5 | const DashLayout = ({ children }) => { 6 | return ( 7 | 8 |
9 | 10 | {children} 11 |
12 |
13 | ); 14 | }; 15 | 16 | export default DashLayout; 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | #DO NOT USE: Docker implementation not available. Wrk2 not easily implementable 2 | FROM node:18.15 3 | 4 | WORKDIR /app 5 | 6 | COPY package*.json ./ 7 | 8 | RUN npm install 9 | 10 | RUN apt-get -y update \ 11 | && apt-get -y install build-essential libssl-dev git zlib1g-dev 12 | 13 | RUN git clone https://github.com/giltene/wrk2.git wrk2 \ 14 | && cd wrk2 \ 15 | && make 16 | 17 | RUN mv wrk2/wrk /usr/local/bin/wrk2 18 | 19 | COPY . . 20 | 21 | EXPOSE 3100 22 | 23 | CMD npm run dev -------------------------------------------------------------------------------- /src/lib/grafanaUrls.json: -------------------------------------------------------------------------------- 1 | [ 2 | "http://localhost:4000/d-solo/rYdddlPWj/node-exporter-full?orgId=1&refresh=15s&from=1678997729717&to=1678998029717&panelId=9", 3 | "http://localhost:4000/d-solo/rYdddlPWj/node-exporter-full?orgId=1&refresh=15s&from=1678997897797&to=1678998197797&panelId=33", 4 | "http://localhost:4000/d-solo/rYdddlPWj/node-exporter-full?orgId=1&refresh=15s&from=1678998069247&to=1678998369247&panelId=37", 5 | "http://localhost:4000/d-solo/rYdddlPWj/node-exporter-full?orgId=1&refresh=15s&from=1678998088512&to=1678998388512&panelId=35" 6 | ] 7 | -------------------------------------------------------------------------------- /methods/getHistogramData.ts: -------------------------------------------------------------------------------- 1 | export const getHistogramData = async () => { 2 | let plotData = []; 3 | // fetch data from the database through the api 4 | await fetch('/api/createHistogram') 5 | .then((res) => res.json()) 6 | .then((data) => { 7 | if (data.plot.length === 0 || !data) { 8 | throw new Error('The file is empty'); 9 | } 10 | plotData = data.plot; 11 | }) 12 | .catch((err) => console.log('Error in fetching plot data from api', err)); 13 | // return the plot data for the chart, this is a single trace 14 | return { 15 | plotData: plotData, 16 | }; 17 | }; -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './app/**/*.{js,ts,jsx,tsx}', 5 | './pages/**/*.{js,ts,jsx,tsx}', 6 | './components/**/*.{js,ts,jsx,tsx}', 7 | 8 | // Or if using `src` directory: 9 | './src/**/*.{js,ts,jsx,tsx}', 10 | ], 11 | theme: { 12 | extend: { 13 | colors: { 14 | midnight: 'rgb(18, 16, 99)', 15 | darkblue: 'rgb(1,19,37)', 16 | pracblack: 'rgb(0,0,14)', 17 | }, 18 | boxShadow: { 19 | 'sm-right': '2px 5px 5px 0 rgba(200, 200, 200, 0.5)', 20 | }, 21 | }, 22 | }, 23 | plugins: [], 24 | }; 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # next.js 12 | .next/ 13 | out/ 14 | 15 | # production 16 | build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | next-env.d.ts 32 | 33 | # vercel 34 | .vercel 35 | 36 | # script files 37 | execWrk2Script.sh 38 | 39 | # wrkScript lua file 40 | wrkScript.lua 41 | 42 | # result txt file 43 | result.txt 44 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": false, 7 | "forceConsistentCasingInFileNames": true, 8 | "noEmit": true, 9 | "incremental": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "paths": { 17 | "@/*": ["./src/*"] 18 | } 19 | }, 20 | "include": [ 21 | "next-env.d.ts", 22 | "**/*.ts", 23 | "**/*.tsx", 24 | "methods/getHistogramData.ts" 25 | ], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /src/lib/histogramCache.json: -------------------------------------------------------------------------------- 1 | [{"x":[0,10,20,30,40,50,55.00000000000001,60,65,70,75,77.5,80,82.5,85,87.5,88.75,90,91.25,92.5,93.75,94.375,95,95.625,96.25,96.875,97.1875,97.5,97.8125,98.125,98.4375,98.5938,98.75,98.9062,99.0625,99.2188,99.2969,99.375,99.4531,99.5313,99.60940000000001,99.64840000000001,99.6875,99.7266,99.76559999999999,99.8047],"y":[0.507,0.985,1.18,1.392,1.579,1.805,1.944,2.046,2.183,2.269,2.409,2.507,2.669,2.861,3.029,3.357,3.559,3.747,3.941,4.283,4.487,4.651,4.935,5.547,5.803,6.235,6.259,6.575,6.867,7.451,7.607,7.607,7.667,8.743,8.743,9.951,9.951,9.951,10.215,10.215,12.055,12.055,12.055,12.055,12.055,12.527],"type":"scatter","mode":"lines+markers","hovertemplate":"X: %{x}%
Y: %{y}ms","marker":{"color":"rgb(203, 78, 219)"}}] -------------------------------------------------------------------------------- /components/Chart/Chart.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic'; 2 | import React from 'react'; 3 | import { PlotParams } from 'react-plotly.js'; 4 | 5 | const Plot = dynamic(() => import('react-plotly.js'), { 6 | ssr: false, 7 | loading: () =>

Loading...

8 | }); 9 | 10 | type ChartProps = { 11 | data: PlotParams['data']; 12 | layout: PlotParams['layout']; 13 | }; 14 | const config = { 15 | autosizable: true, 16 | }; 17 | const MyChart: React.FC = ({ data, layout }) => { 18 | return ( 19 |
20 | 26 |
27 | ); 28 | }; 29 | export default MyChart; 30 | -------------------------------------------------------------------------------- /src/pages/api/createBash.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import { promises as fs } from 'fs'; 3 | 4 | export default async function bashScripts( 5 | req: NextApiRequest, 6 | res: NextApiResponse 7 | ) { 8 | if (req.method === 'POST') { 9 | const constants = req.body; 10 | const bashFile = `#!/bin/bash 11 | ulimit -n 65535 12 | wrk2 -t${constants.numOfThreads} -c${constants.numOfUsers} -d${constants.testDuration} -s wrkScript.lua -L -R${constants.throughput} ${constants.rootUrl} > result.txt`; 13 | fs.writeFile('execWrk2Script.sh', `${bashFile}`); 14 | res.status(200).send('bash file created successfully!'); 15 | } else { 16 | res.status(400).send('Invalid request method'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/pages/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Header from '../../components/Header/Header'; 3 | import Head from 'next/head'; 4 | 5 | const Layout = ({ children }) => { 6 | return ( 7 | <> 8 | 9 | strAPI 10 | 11 | 12 | 13 | 14 |
15 |
16 | {children} 17 |
18 | 19 | ); 20 | }; 21 | 22 | export default Layout; 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev -p 3100", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "child_process": "^1.0.2", 12 | "next": "13.2.3", 13 | "plotly.js": "^2.20.0", 14 | "react": "18.2.0", 15 | "react-dom": "18.2.0", 16 | "react-plotly.js": "^2.6.0", 17 | "swr": "^2.1.0" 18 | }, 19 | "devDependencies": { 20 | "@types/node": "18.15.0", 21 | "@types/plotly.js": "^2.12.18", 22 | "@types/react": "18.0.28", 23 | "@types/react-plotly.js": "^2.6.0", 24 | "autoprefixer": "^10.4.14", 25 | "postcss": "^8.4.21", 26 | "tailwindcss": "^3.2.7", 27 | "typescript": "4.9.5" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /components/Panel/Panel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type PanelProps = { 4 | url: string; 5 | index: number; 6 | deletePanel: (id: any) => Promise; 7 | }; 8 | 9 | const Panel = ({ url, index, deletePanel }: PanelProps): JSX.Element => { 10 | return ( 11 |
12 | 21 | 25 |
26 | ); 27 | }; 28 | 29 | export default Panel; 30 | -------------------------------------------------------------------------------- /src/pages/api/execScript.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import { exec } from 'child_process'; 3 | 4 | export default async function scripts( 5 | req: NextApiRequest, 6 | res: NextApiResponse 7 | ) { 8 | exec('ls', (err) => { 9 | if (err) { 10 | console.error(`Error running script: ${err}`); 11 | res.status(500).send('Error running script'); 12 | return; 13 | } 14 | }); 15 | exec('chmod +x execWrk2Script.sh', (err) => { 16 | if (err) { 17 | console.error(`Error running script: ${err}`); 18 | res.status(500).send('Error running script'); 19 | return; 20 | } 21 | }); 22 | exec('./execWrk2Script.sh', (err) => { 23 | if (err) { 24 | console.error(`Error running script: ${err}`); 25 | res.status(500).send('Error running script'); 26 | return; 27 | } 28 | }); 29 | res.status(200).send('running script'); 30 | } 31 | -------------------------------------------------------------------------------- /components/Sidebar/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Link from 'next/link'; 3 | 4 | const Sidebar = () => { 5 | return ( 6 | 19 | ); 20 | }; 21 | 22 | export default Sidebar; 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 OSLabs Beta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Layout from './layout'; 2 | import Image from 'next/image'; 3 | import Link from 'next/link'; 4 | 5 | export default function Home() { 6 | return ( 7 | 8 |
9 |
10 |
11 | strAPI logo 18 |
19 |

20 | Welcome to StrAPI 21 | ! 22 |

23 | 24 |

25 | 26 | Stress test your application to destress your occupation. 27 | 28 |

29 |

30 | Click Dashboard to get started! 31 |

32 |
33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /components/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Link from 'next/link'; 3 | import Image from 'next/image'; 4 | 5 | const Header = () => { 6 | return ( 7 |
8 | 9 | strAPI logo 10 |

11 | Str 12 | API 13 |

14 | 15 | 29 |
30 | ); 31 | }; 32 | 33 | export default Header; 34 | -------------------------------------------------------------------------------- /src/pages/dashboard/constants/constants.ts: -------------------------------------------------------------------------------- 1 | //layout information that is utilized by Plotly to generate line plot 2 | export const plotLayout = { 3 | title: { 4 | text: 'Response Latency', 5 | font: { 6 | color: 'white', 7 | }, 8 | }, 9 | xaxis: { 10 | title: { 11 | text: 'Percentile', 12 | font: { 13 | color: 'white', 14 | }, 15 | }, 16 | tickmode: 'array', 17 | tickvals: [0, 90, 99], 18 | ticktext: ['0%', '90%', '99%'], 19 | tickfont: { 20 | color: '#fff', 21 | }, 22 | font: { 23 | color: 'white', 24 | }, 25 | }, 26 | yaxis: { 27 | title: { 28 | text: 'Latency (milliseconds)', 29 | font: { 30 | color: 'white', 31 | }, 32 | }, 33 | tickfont: { 34 | color: '#fff', 35 | }, 36 | }, 37 | legend: { 38 | bgcolor: 'rgba(119, 119, 119, .05)', 39 | x: 0, 40 | y: -0.15, 41 | font: { 42 | color: 'white', 43 | }, 44 | orientation: 'h', 45 | }, 46 | modebar: { 47 | bgcolor: 'rgba(119, 119, 119, .05)', 48 | color: 'white', 49 | orientation: 'v', 50 | }, 51 | plot_bgcolor: 'transparent', 52 | paper_bgcolor: 'transparent', 53 | responsive: true, 54 | height: 800, 55 | }; -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | /* @import url('https://fonts.googleapis.com/css2?family=MuseoModerno:wght@100;200;300;400;500;600display=swap'); */ 2 | @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@100;300;400;500;700&display=swap'); 3 | 4 | @tailwind base; 5 | @tailwind components; 6 | @tailwind utilities; 7 | 8 | :root { 9 | --bg-darkest: #2c3333; 10 | --bg-darkgreen: #2e4f4f; 11 | --bg-teal: #0e8388; 12 | --bg-light: #cbe4de; 13 | } 14 | 15 | *, 16 | *::before, 17 | *::after { 18 | box-sizing: border-box; 19 | } 20 | 21 | *, 22 | html, 23 | body { 24 | margin: 0; 25 | padding: 0; 26 | color: rgb(225, 225, 225); 27 | font-family: 'Roboto', sans-serif; 28 | font-weight: 500; 29 | } 30 | 31 | .container { 32 | background-color: #2e4f4f; 33 | min-height: 100vh; 34 | width: 100%; 35 | position: relative; 36 | display: flex; 37 | flex-direction: column; 38 | align-items: center; 39 | position: relative; 40 | } 41 | 42 | /* ------------------------------ */ 43 | /* ---------- 404 page ---------- */ 44 | /* ------------------------------ */ 45 | 46 | .notFound { 47 | margin-top: 200px; 48 | width: 100%; 49 | display: flex; 50 | flex-direction: column; 51 | gap: 50px; 52 | justify-content: center; 53 | align-items: center; 54 | font-family: 'Comic Sans MS', 'Comic Sans', cursive; 55 | } 56 | 57 | .modebar { 58 | margin-top: 100px; 59 | background-color: transparent; 60 | } 61 | 62 | input:-webkit-autofill, 63 | textarea:-webkit-autofill, 64 | select:-webkit-autofill { 65 | -webkit-box-shadow: 0 0 0 1000px #1e293b inset !important; 66 | -webkit-text-fill-color: white !important; 67 | } 68 | -------------------------------------------------------------------------------- /src/pages/api/panels.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction; 2 | import type { NextApiRequest, NextApiResponse } from 'next'; 3 | import path from 'path'; 4 | import { promises as fs } from 'fs'; 5 | 6 | export default async function panels( 7 | req: NextApiRequest, 8 | res: NextApiResponse 9 | ) { 10 | const dbUrl = path.join(process.cwd(), '/src/lib/'); 11 | 12 | const METHOD: string | undefined = req.method; 13 | 14 | switch (METHOD) { 15 | case 'GET': 16 | const urlsGET = await fs.readFile(dbUrl + '/grafanaUrls.json', 'utf-8'); 17 | return res.status(200).json(urlsGET); 18 | 19 | case 'POST': 20 | const { newUrl } = req.body; 21 | const urlsPOST = await fs.readFile(dbUrl + '/grafanaUrls.json', 'utf-8'); 22 | const currentUrls = JSON.parse(urlsPOST); 23 | currentUrls.push(newUrl); 24 | await fs.writeFile( 25 | dbUrl + '/grafanaUrls.json', 26 | JSON.stringify(currentUrls) 27 | ); 28 | return res.status(200).json({ message: 'Successfully added panel.' }); 29 | 30 | case 'DELETE': 31 | const { urlIndex } = req.body; 32 | const urlsDELETE = await fs.readFile( 33 | dbUrl + '/grafanaUrls.json', 34 | 'utf-8' 35 | ); 36 | 37 | const allUrls = JSON.parse(urlsDELETE); 38 | 39 | const newUrls = allUrls.filter( 40 | (url: string, index: number) => urlIndex != index 41 | ); 42 | 43 | // save newUrls to file 44 | await fs.writeFile(dbUrl + '/grafanaUrls.json', JSON.stringify(newUrls)); 45 | return res.status(200).json({ message: 'Successfully deleted panel.' }); 46 | 47 | default: 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/pages/api/createLua.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | // import { createLua } from './luaFileGenerator'; 3 | import { promises as fs } from 'fs'; 4 | 5 | export default async function luaScript( 6 | req: NextApiRequest, 7 | res: NextApiResponse 8 | ) { 9 | if (req.method === 'POST') { 10 | const params = req.body; 11 | let luaFile = ['requests = {}\n\n']; 12 | let reqCounter = 0; 13 | let sumOfRatio = -1; 14 | for (let i = 0; i < params.length; i++) { 15 | luaFile.push( 16 | `request${i} = function() 17 | headers = {} 18 | headers["Content-Type"] = "${params[i].contentType}" 19 | ` 20 | ); 21 | if (params[i].body) { 22 | luaFile.push(`body = ${JSON.stringify(params[i].body)}\n`); 23 | } 24 | luaFile.push(`return wrk.format("${params[i].method}", "${params[i].route}", headers, body) 25 | end\n\n`); 26 | 27 | let r = params[i].ratio; 28 | 29 | while (r > 0) { 30 | luaFile.push(`requests[${reqCounter++}] = request${i}\n`); 31 | r--; 32 | sumOfRatio++; 33 | } 34 | 35 | luaFile.push('\n'); 36 | } 37 | 38 | luaFile.push(`request = function() 39 | return requests[math.random(0, ${sumOfRatio})]() 40 | end 41 | 42 | response = function(status, headers, body) 43 | if status ~= 200 then 44 | io.write("------------------------------\\n") 45 | io.write("Response with status: ".. status .."\\n") 46 | io.write("------------------------------\\n") 47 | io.write("[response] Body:\\n") 48 | io.write(body .. "\\n") 49 | end 50 | end`); 51 | let stringifyArr = luaFile.join(''); 52 | fs.writeFile('wrkScript.lua', `${stringifyArr}`); 53 | res.status(200).send('lua file created successfully!'); 54 | } 55 | else { 56 | res.status(400).send('Invalid request method'); 57 | } 58 | } 59 | export const config = { 60 | api: { 61 | externalResolver: true, 62 | }, 63 | } 64 | -------------------------------------------------------------------------------- /src/pages/api/createHistogram.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import fs from 'fs'; 3 | 4 | export default async function histogram( 5 | req: NextApiRequest, 6 | res: NextApiResponse 7 | ) { 8 | const plot = []; 9 | try { 10 | fs.readFile('result.txt', (err, data) => { 11 | if (err) throw err; 12 | 13 | const yValues = []; 14 | const xValues = []; 15 | 16 | // Split the data into an array of lines 17 | const dataStr = data.toString(); 18 | if (!dataStr || dataStr.length === 0) { 19 | return res.status(200).json({ plot }); 20 | } 21 | const lines = dataStr.split('\n'); 22 | 23 | // Find the column headings index 24 | const headingLineIndex = lines.findIndex((line) => 25 | line.startsWith(' Value ') 26 | ); 27 | 28 | // Find the column endings index 29 | const endOfValuesIndex = lines.findIndex((line) => 30 | line.startsWith('#[Mean') 31 | ); 32 | 33 | // Find the data rows and extract the values 34 | const dataLines = lines.slice(headingLineIndex + 2, endOfValuesIndex - 1); 35 | 36 | dataLines.map((line) => { 37 | const values = line.trim().split(/\s+/); 38 | yValues.push(parseFloat(values[0])); 39 | xValues.push(parseFloat(values[1]) * 100); 40 | }); 41 | 42 | // random rgb color generator 43 | function randomColor() { 44 | let r = Math.floor(Math.random() * 255); 45 | let g = Math.floor(Math.random() * 255); 46 | let b = Math.floor(Math.random() * 255); 47 | while (r > 220 && g > 220 && b > 220) { 48 | r = Math.floor(Math.random() * 255); 49 | g = Math.floor(Math.random() * 255); 50 | b = Math.floor(Math.random() * 255); 51 | } 52 | return `rgb(${r}, ${g}, ${b})`; 53 | } 54 | 55 | const obj = { 56 | x: xValues, 57 | y: yValues, 58 | type: 'scatter', 59 | mode: 'lines+markers', 60 | hovertemplate: 'X: %{x}%
Y: %{y}ms', 61 | marker: { color: randomColor() }, 62 | }; 63 | 64 | plot.push(obj); 65 | 66 | res.status(200).json({ plot }); 67 | }); 68 | } catch (error) { 69 | res.status(500).json({ message: 'Error occurred', error: error.message }); 70 | } 71 | } 72 | 73 | export const config = { 74 | api: { 75 | externalResolver: true, 76 | }, 77 | } 78 | -------------------------------------------------------------------------------- /src/pages/dashboard/indexComponents/currentMethods.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { CurrentMethodsContext } from "../index"; 3 | 4 | export const CurrentMethods = () => { 5 | 6 | const { methods, setMethods, ratioSum, setRatioSum } = useContext(CurrentMethodsContext); 7 | //delete params from methods array 8 | const deleteMethod = (el: any) => { 9 | const idx = el.id; 10 | setRatioSum(Number(ratioSum) - Number(methods[idx].ratio)); 11 | const newMethods = methods; 12 | newMethods.splice(idx, 1); 13 | setMethods([...newMethods]); 14 | }; 15 | return( 16 |
17 |

Current Methods:

18 |
19 |

Route

20 |

HTTP Method

21 |

Request Body

22 |

Ratio

23 |

Delete Method

24 |
25 |
    26 | {methods.map((method, index) => { 27 | return ( 28 |
  • 33 |

    {method.route}

    34 |

    {method.method}

    35 | {method.method === 'POST' ? ( 36 |

    37 | {method.body} 38 |

    39 | ) : ( 40 | 'N/A' 41 | )} 42 |

    43 | {method.ratio}:{ratioSum} 44 |

    45 |
    46 | 53 |
    54 |
  • 55 | ); 56 | })} 57 |
58 |
59 | ) 60 | } -------------------------------------------------------------------------------- /src/pages/api/histogramCache.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction; 2 | import type { NextApiRequest, NextApiResponse } from 'next'; 3 | import path from 'path'; 4 | import { promises as fs } from 'fs'; 5 | 6 | export default async function panels( 7 | req: NextApiRequest, 8 | res: NextApiResponse 9 | ) { 10 | // shortcut to the database folder 11 | const dbUrl = path.join(process.cwd(), '/src/lib/'); 12 | // get the request method 13 | const METHOD: string = req.method; 14 | // process the request based on the method 15 | switch (METHOD) { 16 | // if the request is a GET request, return the current traces from the database, which is a JSON file 17 | case 'GET': 18 | // read the file and return the JSON 19 | const tracesGET = await fs.readFile( 20 | dbUrl + '/histogramCache.json', 21 | 'utf-8' 22 | ); 23 | return res.status(200).json(tracesGET); 24 | // if the request is a POST request, add the new trace to the database 25 | case 'POST': 26 | // get the new trace from the request body 27 | const { newTrace } = req.body; 28 | // read the current traces from the database 29 | const tracesPOST = await fs.readFile( 30 | dbUrl + '/histogramCache.json', 31 | 'utf-8' 32 | ); 33 | const currentTraces = JSON.parse(tracesPOST); 34 | // add the new trace to the current traces 35 | currentTraces.push(newTrace); 36 | // write the new traces to the database 37 | await fs.writeFile( 38 | dbUrl + '/histogramCache.json', 39 | JSON.stringify(currentTraces) 40 | ); 41 | // return a success message 42 | return res.status(200).json({ message: 'Successfully updated graph.' }); 43 | // if the request is a DELETE request, delete the trace from the database 44 | case 'DELETE': 45 | const { traceIndex } = req.body; 46 | // if traceIndex is -1, delete all traces 47 | if (traceIndex === -1) { 48 | await fs.writeFile(dbUrl + '/histogramCache.json', JSON.stringify([])); 49 | return res.status(200).json({ message: 'Successfully reset graph.' }); 50 | } 51 | 52 | const tracesDELETE = await fs.readFile( 53 | dbUrl + '/histogramCache.json', 54 | 'utf-8' 55 | ); 56 | 57 | const allTraces = JSON.parse(tracesDELETE); 58 | const newTraces = allTraces.filter( 59 | (_: string, index: number) => traceIndex != index 60 | ); 61 | 62 | await fs.writeFile( 63 | dbUrl + '/histogramCache.json', 64 | JSON.stringify(newTraces) 65 | ); 66 | 67 | return res.status(200).json({ message: 'Successfully reset graph.' }); 68 | 69 | default: 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/pages/about/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Layout from '../layout'; 3 | import Link from 'next/link'; 4 | import Image from 'next/image'; 5 | 6 | const index = () => { 7 | const aboutText = 'm-6 text-xl'; 8 | 9 | return ( 10 | 11 |
12 |
13 |

14 | About StrAPI 15 |

16 |

17 | StrAPI is designed to be a free and open-source alternative to 18 | load-test your API routes. 19 |

20 |

21 | Testing API routes and monitoring the health of the containers does 22 | not need to be difficult. In fact, it should be relatively easy. 23 |

24 |

25 | That's where StrAPI comes in! StrAPI is a light-weight program that 26 | allows you to visualize and test your API routes, free of charge 27 | with peace of mind that everything is working as intended! 28 |

29 |
30 |
31 |

32 | Why StrAPI stands out 33 |

34 |
    35 |
  • 36 |

    Cost Optimization

    37 |

    38 | Quick cost estimates and insights to stay within budget 39 | constraints. 40 |

    41 |
  • 42 |
  • 43 |

    Data Visualization

    44 |

    45 | Clear and interactive visual representation of cluster metric 46 | data. 47 |

    48 |
  • 49 |
  • 50 |

    User-friendly Interface

    51 |

    Easy to use interface for quick access to information.

    52 |
  • 53 |
  • 54 |

    Efficiency

    55 |

    Streamlines and improves the overall deployment process.

    56 |
  • 57 |
58 |
59 | 60 |
61 |

Installation

62 |
63 | {/*

Github Read Me

*/} 64 | github logo 70 | 75 | GitHub ReadMe 76 | 77 |
78 |
79 |
80 |
81 | ); 82 | }; 83 | 84 | export default index; 85 | -------------------------------------------------------------------------------- /src/pages/dashboard/indexComponents/plotlyChart.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect } from "react"; 2 | import { getHistogramData } from '../../../../methods/getHistogramData'; 3 | import { PlotlyChartContext } from '../index'; 4 | import MyChart from '../../../../components/Chart/Chart'; 5 | 6 | export const PlotlyChart = () => { 7 | const { plotData, plotLayout, showDropdown, setShowDropdown, addTrace, getTraces } = useContext(PlotlyChartContext) 8 | 9 | //reveal dropdown options. toggle show/hide 10 | const revealDropdownOptions = () => { 11 | setShowDropdown(!showDropdown); 12 | }; 13 | 14 | 15 | const deleteTrace = async (id: Number): Promise => { 16 | const body = { 17 | traceIndex: Number(id), 18 | }; 19 | 20 | try { 21 | await fetch('/api/histogramCache', { 22 | method: 'DELETE', 23 | headers: { 'Content-Type': 'Application/JSON' }, 24 | body: JSON.stringify(body), 25 | }); 26 | getTraces(); 27 | } catch (err) { 28 | throw new Error('Unable to delete traces.'); 29 | } 30 | }; 31 | //if test has run and graph did not generate, but result.txt was created, this function will manually generate the results to put into plotly. can also double as a color picker 32 | async function manuallyRequestPlotData() { 33 | const response = await getHistogramData(); 34 | addTrace(response.plotData[0]); 35 | } 36 | return ( 37 |
38 |
39 | 40 |
41 |

42 |
43 | 49 |
50 | 56 | {showDropdown && ( 57 |
58 | {Array.from(Array(plotData.length), (_,index) => ( 59 | 68 | ))} 69 | 79 |
80 | )} 81 |
82 |
83 | ) 84 | } -------------------------------------------------------------------------------- /src/pages/dashboard/metrics.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useRef } from 'react'; 2 | import DashLayout from './layout'; 3 | import Panel from '../../../components/Panel/Panel'; 4 | import { FC } from 'react'; 5 | 6 | const Metrics: FC = () => { 7 | const [urls, setUrls] = useState([]); 8 | const [isLoading, setIsLoading] = useState(false); 9 | 10 | const srcRef = useRef(null); 11 | 12 | // fetches data from /api/panels.ts. Next.js does not recommend fetching like this, but in order to get responsive state and complete our read/write operations to our JSON file, this is the best approach. 13 | const getData = async (): Promise => { 14 | const res = await fetch('/api/panels', { 15 | method: 'GET', 16 | headers: { 'Content-Type': 'Application/JSON' }, 17 | }); 18 | const data = await res.json(); 19 | // NOTE: we are using a .json file as our storage, so we also need to parse the file contents using JSON.parse() 20 | const panelUrls: string[] = JSON.parse(data); 21 | setUrls(panelUrls); 22 | setIsLoading(false); 23 | }; 24 | 25 | // runs on initial page render 26 | useEffect(() => { 27 | setIsLoading(true); 28 | getData(); 29 | }, []); 30 | 31 | // adds a new panel to the dashboard 32 | const addPanel = async (): Promise => { 33 | const body = { 34 | newUrl: srcRef?.current?.value, 35 | }; 36 | 37 | try { 38 | await fetch('/api/panels', { 39 | method: 'POST', 40 | headers: { 'Content-Type': 'Application/JSON' }, 41 | body: JSON.stringify(body), 42 | }); 43 | getData(); 44 | } catch (err) { 45 | throw new Error('Unable to add iframe src.'); 46 | } 47 | }; 48 | // deletes a panel from the dashboard 49 | const deletePanel = async (id: any): Promise => { 50 | const body = { 51 | urlIndex: Number(id), 52 | }; 53 | 54 | try { 55 | await fetch('/api/panels', { 56 | method: 'DELETE', 57 | headers: { 'Content-Type': 'Application/JSON' }, 58 | body: JSON.stringify(body), 59 | }); 60 | getData(); 61 | } catch (err) { 62 | throw new Error('Unable to delete panel.'); 63 | } 64 | }; 65 | 66 | return ( 67 | 68 |
69 |
70 |

Live System Metrics:

71 |

Courtesy of Grafana ©

72 | 73 | 74 | 80 | 86 |
87 | 88 | {isLoading ? ( 89 |

Loading metrics...

90 | ) : ( 91 | urls.map((url: string, index: number) => { 92 | return ( 93 | 99 | ); 100 | }) 101 | )} 102 |
103 |
104 | ); 105 | }; 106 | 107 | export default Metrics; 108 | -------------------------------------------------------------------------------- /src/pages/dashboard/indexComponents/inputData.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { InputDataContext } from "../index"; 3 | 4 | export const InputData = () => { 5 | //tailwind styling 6 | const inputStyle = 'h-10 text-lg bg-slate-800 text-white p-3 rounded-md border border-slate-700 shadow shadow-slate-500'; 7 | 8 | const { constants, setConstants, params, setParams, isPost, setIsPost, ratioSum, setRatioSum, methods, setMethods, startTest } = useContext(InputDataContext); 9 | 10 | //add params into methods array 11 | const addMethod = () => { 12 | setRatioSum(Number(ratioSum) + Number(params.ratio)); 13 | setMethods(methods.concat(params)); 14 | }; 15 | 16 | return ( 17 |
18 |
19 |

Testing Constants:

20 | 21 | 23 | setConstants({ ...constants, rootUrl: e.target.value }) 24 | } 25 | value={constants.rootUrl} 26 | id="root-url" 27 | className={inputStyle} 28 | type="text" 29 | placeholder="http://localhost:" 30 | /> 31 | 32 | 34 | setConstants({ ...constants, numOfThreads: e.target.value }) 35 | } 36 | value={constants.numOfThreads} 37 | id="number-of-threads" 38 | className={inputStyle} 39 | type="text" 40 | placeholder="2" 41 | /> 42 | 43 | 45 | setConstants({ ...constants, testDuration: e.target.value }) 46 | } 47 | value={constants.testDuration} 48 | id="test-duration" 49 | className={inputStyle} 50 | type="text" 51 | placeholder="60" 52 | /> 53 | 54 | 56 | setConstants({ ...constants, numOfUsers: e.target.value }) 57 | } 58 | value={constants.numOfUsers} 59 | id="connections" 60 | className={inputStyle} 61 | type="text" 62 | placeholder="50" 63 | /> 64 | 65 | 67 | setConstants({ ...constants, throughput: e.target.value }) 68 | } 69 | value={constants.throughput} 70 | id="throughput" 71 | className={inputStyle} 72 | type="text" 73 | placeholder="100" 74 | /> 75 | 76 | 77 |
78 |

Testing Parameters

79 | 80 | 81 | 83 | setParams({ ...params, route: e.target.value }) 84 | } 85 | value={params.route} 86 | id="route" 87 | className={inputStyle} 88 | type="text" 89 | placeholder="/api/route" 90 | /> 91 | 92 | 104 | 105 | 107 | setParams({ ...params, ratio: e.target.value }) 108 | } 109 | value={params.ratio} 110 | id="ratio" 111 | className={inputStyle} 112 | type="text" 113 | placeholder="1" 114 | /> 115 | 116 | {isPost ? ( 117 |
118 |

POST Request Body:

119 |

(Must be in JSON format)

120 | 134 |
135 | ) : null} 136 | 137 | 144 | 151 |
152 |
153 | ) 154 | } -------------------------------------------------------------------------------- /src/pages/dashboard/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, createContext } from 'react'; 2 | import Head from 'next/head'; 3 | import DashLayout from './layout'; 4 | import { InputData } from './indexComponents/inputData'; 5 | import { CurrentMethods } from './indexComponents/currentMethods'; 6 | import { PlotlyChart } from './indexComponents/plotlyChart'; 7 | import { testingConstants, params, methods } from './types/types'; 8 | import { plotLayout } from './constants/constants'; 9 | import { getHistogramData } from '../../../methods/getHistogramData'; 10 | 11 | export const InputDataContext = createContext(null); 12 | export const CurrentMethodsContext = createContext(null); 13 | export const PlotlyChartContext = createContext(null); 14 | 15 | const index = () => { 16 | 17 | //initializing constants to their default values 18 | const initialConstants: testingConstants = { 19 | rootUrl: '', 20 | numOfThreads: 2, 21 | testDuration: 5, 22 | numOfUsers: 50, 23 | throughput: 100, 24 | }; 25 | 26 | //iniitalizing parameters to default values 27 | const initialParams: params = { 28 | route: '', 29 | method: 'GET', 30 | body: '', 31 | contentType: 'application/json', 32 | ratio: 1, 33 | }; 34 | 35 | const initialPlotData = []; 36 | const initialMethods: methods = []; 37 | 38 | const [constants, setConstants] = useState(initialConstants); 39 | const [params, setParams] = useState(initialParams); 40 | const [isPost, setIsPost] = useState(false); 41 | const [methods, setMethods] = useState(initialMethods); 42 | const [ratioSum, setRatioSum] = useState(0); 43 | const [plotData, setPlotData] = useState(initialPlotData); 44 | const [, setIsLoading] = useState(false); 45 | const [showDropdown, setShowDropdown] = useState(false); 46 | 47 | //executes stress test run by Wrk2 48 | const startTest = async (constants:testingConstants, methods:[params]): Promise => { 49 | //creates bash file through utilizing constants to create wrk2 bash command run in terminal 50 | await fetch('/api/createBash', { 51 | method: 'POST', 52 | body: JSON.stringify(constants), 53 | headers: { 54 | 'Content-Type': 'application/json', 55 | }, 56 | }); 57 | 58 | //creates lua file that is the blueprint for the wrk2 script 59 | await fetch('/api/createLua', { 60 | method: 'POST', 61 | body: JSON.stringify(methods), 62 | headers: { 63 | 'Content-Type': 'application/json', 64 | }, 65 | }); 66 | 67 | //executes bash file once lua file is generated 68 | await fetch('/api/execScript'); 69 | 70 | //starts async function retryGetHistogramData() after setTimeout 71 | setTimeout(async () => { 72 | await retryGetHistogramData(); 73 | }, constants.testDuration * 1000); 74 | }; 75 | 76 | //after test has been executed, the result.txt file will be read and scrape the necessary data to be input in plotly. will test once every second for 30 seconds until success. 77 | async function retryGetHistogramData(maxRetries = 300, delay = 1000) { 78 | let currentRetry = 0; 79 | 80 | while (currentRetry <= maxRetries) { 81 | try { 82 | const response = await getHistogramData(); 83 | if (response.plotData.length > 0) { 84 | addTrace(response.plotData[0]); 85 | return; 86 | } 87 | } catch (error) { 88 | currentRetry++; 89 | if (currentRetry >= maxRetries) { 90 | throw new Error(`Failed after ${maxRetries} retries`); 91 | } 92 | } 93 | 94 | // Wait for the specified delay before retrying 95 | await new Promise((resolve) => setTimeout(resolve, delay)); 96 | } 97 | } 98 | 99 | const addTrace = async (trace): Promise => { 100 | const body = { 101 | newTrace: trace, 102 | }; 103 | 104 | try { 105 | await fetch('/api/histogramCache', { 106 | method: 'POST', 107 | headers: { 'Content-Type': 'Application/JSON' }, 108 | body: JSON.stringify(body), 109 | }); 110 | // Fetches the updated list of traces from the server 111 | getTraces(); 112 | } catch (err) { 113 | throw new Error('Unable to add trace.'); 114 | } 115 | }; 116 | 117 | // fetches data from /api/histogramCache.ts 118 | const getTraces = async (): Promise => { 119 | const res = await fetch('/api/histogramCache', { 120 | method: 'GET', 121 | headers: { 'Content-Type': 'Application/JSON' }, 122 | }); 123 | const data = await res.json(); 124 | 125 | // NOTE: we are using a .json file as our storage, so we also need to parse the file contents using JSON.parse() 126 | const graphTraces: Number[] = JSON.parse(data); 127 | setPlotData(graphTraces); 128 | setIsLoading(false); 129 | }; 130 | 131 | // runs on initial page render to render plotter 132 | useEffect(() => { 133 | setIsLoading(true); 134 | getTraces(); 135 | }, []); 136 | 137 | 138 | return ( 139 | 140 | 141 | Dashboard 142 | 143 |
144 | {/*
*/} 145 |

Input Required Data:

146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 |
160 |
161 | ); 162 | }; 163 | export default index; 164 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Strapi

2 |
3 |
4 | 5 | 6 |

7 | React Performance Tool 8 |

9 | 10 |

Free and open-source application for stress-testing RESTful API routes!

11 | 12 | *(Now Docker Compatible!)* 13 |
14 |
15 |
16 | 17 | Next.js 18 | 19 | 20 | React 21 | 22 | TypeScript 23 | 24 | Grafana 25 | 26 | Docker 27 | 28 | Prometheus 29 | 30 | Lua 31 | 32 | Tailwind CSS 33 |
34 | 35 | --- 36 | 37 | ## Table of Contents 38 | 39 | - [Features](#features) 40 | - [Getting Started](#getting-started) 41 | - [Viewing Test Results](#viewing-test-results) 42 | - [Future Plans](#future-plans) 43 | ## Features 44 | 45 | ### Testing: 46 | This section is dedicated to robust HTTP route testing and benchmarking. It offers customizable configurations and employs the Wrk2 benchmarking tool, streamlining performance evaluation. Wrk2 data is extracted and visualized with Plotly, enabling quick assessment of response times and ensuring the application meets service level expectations. 47 | 48 | ![Alt Text](/public/run-first-test.gif) 49 | 50 | ### Viewing: 51 | Strapi focuses on simplifying the creation and visualization of real-time performance metrics. By seamlessly integrating Grafana with Prometheus data, we harness Grafana's extensive library of templates designed for stress testing metrics and data exporters. Prometheus collects essential metrics, enabling the creation of a performance observability dashboard within Grafana. 52 | 53 | ![Alt Text](/public/run-multiple-tests.gif) 54 | 55 | ## Getting Started 56 | 57 | ### Docker Installation: 58 | - For Docker usage, please see Docker Repo to pull and run Docker image. 59 | 60 | ### Prerequisites: 61 | 62 | - If you do not have it, install Wrk2. This can be installed via Homebrew on MacOS. 63 | - NOTE: Mac devices using the M1 chip may require additional configuration setting up Wrk2. 64 | - Linux users can install Wrk2 via the following commands: 65 | ``` 66 | sudo apt-get install -y build-essential libssl-dev git zlib1g-dev 67 | git clone https://github.com/giltene/wrk2.git 68 | cd wrk2 69 | make 70 | ``` 71 | - A file named wrk will be generated in that directory, rename wrk to wrk2, and then run 72 | ``` 73 | sudo cp wrk2 /usr/local/bin 74 | ``` 75 | - Make sure your server is up and running. If you are in development mode, the server URL should be something like `http://localhost:1234`, with the 1-2-3-4 being your server's port number. 76 | - If your application is running in a Docker container, make sure to have your local port exposed or access to the container's ip address, and ensure you are referencing the right port number. 77 | - If you are testing a database, make sure to have it configured to a test environment. 78 | 79 | ### Instructions: 80 | 81 | 1. Fork and clone this repository to your local machine. 82 | 2. Ensure your server's API endpoints are exposed and your server is running. 83 | 3. Within the StrAPI directory, run 84 | ``` 85 | npm install 86 | npm run dev 87 | ``` 88 | 4. In your browser, go to http://localhost:3100 89 | 5. Click the "Dashboard" link located on the header at the top-right of the page. 90 | 6. Input all required testing parameters as shown on the Dashboard page. 91 | - If you are testing POST requests, format the body of the request in JSON format with key-value pairs. 92 | 7. After all required parameters are input, hit "Add Method" to include method on the test. 93 | 8. After all desired methods added, hit "Start Test" to begin testing. 94 | 95 | 96 | ### Viewing Test Results 97 | ![Alt Text](/public/grafana-metrics.png) 98 | 1. After the test is complete, a trace will be generated and displayed below on the latency graph. 99 | 2. Subsequent tests will be added to the graph, allowing you to compare the performance of different endpoints and tests. 100 | 3. Traces can be removed individually or all at once by clicking the Remove Traces button. 101 | 4. If the trace is not automatically displayed on the graph after the test is complete, click the "Request Plot Data Manually" button to display the trace. 102 | 5. StrAPI supports Grafana integration. To view Grafana panels click on Grafana Metrics in the left panel. 103 | 6. In the input field, enter the grafana iframe url and click "Add Panel". 104 | 7. To remove a panel, click on the "Remove Panel" button above the panel. 105 | 106 | 107 | ### Future Plans 108 | 109 | - Customize Grafana and Prometheus containers to work with StrAPI by configuring docker compose. 110 | - Add support for more HTTP methods, such as PUT, PATCH, DELETE, etc. 111 | - Containerize StrAPI alongside Wrk2 to allow for easy deployment. 112 | - Utilize (NoSQL/SQL) database with authentication to store test results and user data. 113 | - Host StrAPI on AWS or other providers. 114 | - Expand testing suite to include more tests 115 | - Add support for Kubernetes & cluster monitoring 116 | - Add support for web servers like Nginx and Apache. 117 | - Add support for message brokers and event streamers like RabbitMQ and Kafka. 118 | --------------------------------------------------------------------------------