├── client ├── src │ ├── vite-env.d.ts │ ├── assets │ │ ├── lambda.png │ │ └── lambda-icon-32x32.svg │ ├── chartSetup.ts │ ├── main.tsx │ ├── components │ │ ├── RowComponent.tsx │ │ ├── PercentileLatencyComponent.tsx │ │ ├── ColdStartsGraphComponent.tsx │ │ ├── AvgBilledDurGraphComponent.tsx │ │ ├── TotalDurationComponent.tsx │ │ ├── ThrottleComponent.tsx │ │ ├── ConcurrExecComponent.tsx │ │ ├── ConfigPageComponent.tsx │ │ ├── NavbarComponent.tsx │ │ └── ConfigPageComponent.scss │ ├── containers │ │ ├── ConfigPageContainer.scss │ │ ├── ColdStartsMetricsContainer.tsx │ │ ├── ConfigPageContainer.tsx │ │ ├── DashboardContainer.tsx │ │ ├── ChatContainer.tsx │ │ ├── CloudwatchContainer.scss │ │ └── CloudwatchContainer.tsx │ ├── App.tsx │ ├── NavbarComponent.css │ ├── Graphs.css │ ├── App.css │ └── App.scss ├── tsconfig.json ├── vite.config.ts ├── .gitignore ├── index.html ├── tsconfig.node.json ├── tsconfig.app.json ├── eslint.config.js ├── package.json └── README.md ├── .gitignore ├── server ├── .gitignore ├── types.ts ├── models │ ├── lambdaModel.ts │ ├── visDataModel.ts │ └── ConversationModel.ts ├── server.js ├── routes │ ├── configRoutes.ts │ └── dataRoutes.ts ├── controllers │ ├── getFunctionsController.ts │ ├── connectDatabaseController.ts │ ├── envController.ts │ ├── databaseController.ts │ ├── percentileController.ts │ ├── ChatController.ts │ ├── cloudWatchController.ts │ └── rawDataController.ts ├── configs │ └── awsconfig.ts ├── package.json ├── server.ts └── tsconfig.json ├── LICENSE └── README.md /client/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /client/src/assets/lambda.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Lambda-Lens/HEAD/client/src/assets/lambda.png -------------------------------------------------------------------------------- /client/src/chartSetup.ts: -------------------------------------------------------------------------------- 1 | import { Chart, registerables } from 'chart.js'; 2 | 3 | Chart.register(...registerables); -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /client/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | server: { port: 3000 }, 8 | }); 9 | -------------------------------------------------------------------------------- /client/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import App from './App.js'; 4 | import './App.scss'; 5 | 6 | 7 | createRoot(document.getElementById('root')!).render( 8 | 9 | 10 | , 11 | ); 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore the .env file in the server directory 2 | /server/.env 3 | 4 | # Ignore the node_modules directory 5 | /node_modules 6 | 7 | #Ignore node modules in server 8 | server/node_modules/ 9 | 10 | # Ignore TypeScript build outputs 11 | /server/dist 12 | 13 | # Ignore other specific directories or files if needed 14 | /server/logs 15 | /server/tmp 16 | 17 | dist 18 | package-lock.json 19 | .DS_Store -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | .env 27 | package-lock.json -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | package-lock.json 27 | .DS_Store -------------------------------------------------------------------------------- /server/types.ts: -------------------------------------------------------------------------------- 1 | export interface Config { 2 | credentials: { 3 | accessKeyId: string; 4 | secretAccessKey: string; 5 | }; 6 | region: string; 7 | } 8 | 9 | export type GetAwsConfig = () => Config; 10 | 11 | export interface FormattedLog { 12 | Date: string; 13 | Time: string; 14 | BilledDuration?: string; 15 | MaxMemUsed?: string; 16 | InitDuration?: string; 17 | FunctionName?: string; 18 | } 19 | -------------------------------------------------------------------------------- /server/models/lambdaModel.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const logSchema = new mongoose.Schema( 4 | { 5 | Date: String, 6 | Time: String, 7 | FunctionName: String, 8 | Duration: String, 9 | BilledDuration: String, 10 | InitDuration: String, 11 | MaxMemUsed: String, 12 | }, 13 | { timestamps: true } 14 | ); 15 | 16 | const Log = mongoose.model('Log', logSchema); 17 | export default Log; 18 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | Lambda Lens 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | const express_1 = __importDefault(require("express")); 7 | const app = (0, express_1.default)(); 8 | const PORT = 8080; 9 | app.get('/', (req, res, next) => { 10 | res.send('hello'); 11 | }); 12 | app.listen(PORT, () => { 13 | console.log(`Running on Port ${PORT}`); 14 | }); 15 | -------------------------------------------------------------------------------- /server/models/visDataModel.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model } from 'mongoose'; 2 | 3 | const visDataSchema = new Schema( 4 | { 5 | region: { type: String, required: true }, 6 | functionName: { type: String, required: true }, 7 | avgBilledDur: { type: Number, required: true }, 8 | numColdStarts: { type: Number, required: true }, 9 | percentColdStarts: { type: Number, required: true }, 10 | }, 11 | { timestamps: true } 12 | ); 13 | 14 | const visData = model('visData', visDataSchema); 15 | 16 | export default visData; 17 | -------------------------------------------------------------------------------- /client/src/components/RowComponent.tsx: -------------------------------------------------------------------------------- 1 | import '../Graphs.css'; 2 | 3 | const RowComponent = ({ 4 | functionName, 5 | avgBilledDur, 6 | coldStarts, 7 | percentage 8 | }: { 9 | functionName: string, 10 | avgBilledDur: number, 11 | coldStarts: number, 12 | percentage: number 13 | }) => { 14 | return ( 15 |
16 |
{functionName}
17 |
{avgBilledDur} ms
18 |
{coldStarts}
19 |
{percentage}%
20 |
21 | ); 22 | }; 23 | 24 | export default RowComponent; -------------------------------------------------------------------------------- /client/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": ["ES2023"], 5 | "module": "ESNext", 6 | "skipLibCheck": true, 7 | 8 | /* Bundler mode */ 9 | "moduleResolution": "bundler", 10 | "allowImportingTsExtensions": true, 11 | "isolatedModules": true, 12 | "moduleDetection": "force", 13 | "noEmit": true, 14 | 15 | /* Linting */ 16 | "strict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noFallthroughCasesInSwitch": true 20 | }, 21 | "include": ["vite.config.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /client/src/containers/ConfigPageContainer.scss: -------------------------------------------------------------------------------- 1 | .config-page-container { 2 | display: flex; 3 | // margin-top: -15px; 4 | padding: 20px; 5 | background-color: #ffffff; 6 | border-radius: 4px; 7 | font-family: Helvetica, Arial, sans-serif; 8 | flex-direction: column; 9 | align-items: center; 10 | 11 | h2 { 12 | margin-right: 16px; 13 | font-weight: 400; 14 | padding-left: 20px; 15 | font-size: 48px; 16 | align-self: flex-start; 17 | } 18 | 19 | .config-component { 20 | display: flex; 21 | font-family: Helvetica, Arial, sans-serif; 22 | flex-direction: column; 23 | align-items: center; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /client/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"] 24 | } 25 | -------------------------------------------------------------------------------- /server/routes/configRoutes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { envController } from '../controllers/envController'; 3 | import { Request, Response, NextFunction } from 'express'; 4 | import { connectDatabaseController } from '../controllers/connectDatabaseController'; 5 | 6 | const router = Router(); 7 | 8 | router.post('/save', envController.saveSecrets, (_req: Request, res: Response, next: NextFunction) => { 9 | // console.log('in the /api/config/save endpoint'); 10 | return next(); 11 | }); 12 | 13 | router.get('/db', connectDatabaseController.connectDatabase, (_req: Request, res: Response, next: NextFunction) => { 14 | // console.log('in the /api/config/db endpoint'); 15 | return next(); 16 | }); 17 | 18 | export default router -------------------------------------------------------------------------------- /server/controllers/getFunctionsController.ts: -------------------------------------------------------------------------------- 1 | import { LambdaClient, ListFunctionsCommand, ListFunctionsCommandOutput } from '@aws-sdk/client-lambda'; 2 | import { getAwsConfig } from '../configs/awsconfig'; 3 | 4 | 5 | export const getFunction = async (): Promise => { 6 | const awsconfig = getAwsConfig(); 7 | const command = new ListFunctionsCommand({}); 8 | const lambdaClient = new LambdaClient(awsconfig); 9 | try { 10 | const data: ListFunctionsCommandOutput = await lambdaClient.send(command); 11 | const funcObjectArray = data.Functions || []; 12 | return funcObjectArray.map(func => func.FunctionName || ''); 13 | } catch (err) { 14 | console.error('Error fetching Lambda functions:', err); 15 | return []; 16 | } 17 | }; -------------------------------------------------------------------------------- /server/models/ConversationModel.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema, Document } from 'mongoose'; 2 | 3 | interface ConversationEntry { 4 | role: 'user' | 'assistant'; 5 | content: string; 6 | } 7 | 8 | interface ConversationDocument extends Document { 9 | id: string; 10 | conversation: ConversationEntry[]; 11 | lastUpdated: Date; 12 | } 13 | 14 | const ConversationSchema = new Schema({ 15 | id: { type: String, required: true, unique: true }, 16 | conversation: [{ role: { type: String, enum: ['user', 'assistant'], required: true }, content: { type: String, required: true } }], 17 | lastUpdated: { type: Date, default: Date.now } 18 | }); 19 | 20 | const ConversationModel = mongoose.model('Conversation', ConversationSchema); 21 | 22 | export default ConversationModel; -------------------------------------------------------------------------------- /client/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | 7 | export default tseslint.config( 8 | { ignores: ['dist'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | 'react-refresh/only-export-components': [ 23 | 'warn', 24 | { allowConstantExport: true }, 25 | ], 26 | }, 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /client/src/assets/lambda-icon-32x32.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /server/configs/awsconfig.ts: -------------------------------------------------------------------------------- 1 | import { Config, GetAwsConfig } from '../types.js'; 2 | import dotenv from 'dotenv'; 3 | // import path from 'path'; 4 | 5 | // Load environment variables 6 | // dotenv.config({ path: path.resolve(__dirname, '../.env') }); 7 | 8 | //typescript recognizes process.env as undefined 9 | //use ! to signify that it's NOT null even though it looks like it 10 | export const awsconfig: Config = { 11 | credentials: { 12 | accessKeyId: process.env.AWS_ACCESS_KEY_ID!, 13 | secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, 14 | }, 15 | region: process.env.AWS_REGION!, 16 | }; 17 | 18 | export const getAwsConfig: GetAwsConfig = () => { 19 | dotenv.config(); 20 | 21 | return { 22 | credentials: { 23 | accessKeyId: process.env.AWS_ACCESS_KEY_ID!, 24 | secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, 25 | }, 26 | region: process.env.AWS_REGION!, 27 | }; 28 | }; -------------------------------------------------------------------------------- /client/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'; 2 | import DashboardContainer from './containers/DashboardContainer'; 3 | import CloudwatchContainer from './containers/CloudwatchContainer'; 4 | import NavbarComponent from './components/NavbarComponent'; 5 | import ConfigPageContainer from './containers/ConfigPageContainer'; 6 | import ChatContainer from './containers/ChatContainer'; 7 | import './App.css' 8 | import './chartSetup'; 9 | 10 | function App() { 11 | 12 | return ( 13 | 14 |
15 | 16 | 17 | } /> 18 | } /> 19 | } /> 20 | }/> 21 | 22 |
23 |
24 | ) 25 | } 26 | 27 | export default App 28 | 29 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "main": "server.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1", 7 | "start": "ts-node server.ts", 8 | "dev": "nodemon server.ts", 9 | "build": "tsc --build" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "description": "", 15 | "dependencies": { 16 | "@aws-sdk/client-bedrock": "^3.645.0", 17 | "@aws-sdk/client-bedrock-runtime": "^3.645.0", 18 | "@aws-sdk/client-cloudwatch": "^3.635.0", 19 | "@aws-sdk/client-cloudwatch-logs": "^3.635.0", 20 | "@aws-sdk/client-lambda": "^3.637.0", 21 | "@types/aws-sdk": "^0.0.42", 22 | "@types/node": "^22.5.0", 23 | "body-parser": "^1.20.2", 24 | "cors": "^2.8.5", 25 | "dotenv": "^16.4.5", 26 | "express": "^4.19.2", 27 | "mongoose": "^8.6.0", 28 | "ts-node": "^10.9.2" 29 | }, 30 | "devDependencies": { 31 | "@types/cors": "^2.8.17", 32 | "@types/express": "^4.17.21", 33 | "@types/node": "^22.5.0", 34 | "nodemon": "^3.1.4", 35 | "typescript": "^5.5.4" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Open Source Labs 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 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@emotion/react": "^11.13.3", 14 | "@emotion/styled": "^11.13.0", 15 | "@fontsource/roboto": "^5.0.14", 16 | "@mui/icons-material": "^5.16.7", 17 | "@mui/material": "^5.16.7", 18 | "chart.js": "^4.4.3", 19 | "mongoose": "^8.6.0", 20 | "react": "^18.3.1", 21 | "react-chartjs-2": "^5.2.0", 22 | "react-dom": "^18.3.1", 23 | "react-hook-form": "^7.53.0", 24 | "react-router-dom": "^6.26.1", 25 | "sass": "^1.77.8" 26 | }, 27 | "devDependencies": { 28 | "@eslint/js": "^9.9.0", 29 | "@types/react": "^18.3.3", 30 | "@types/react-dom": "^18.3.0", 31 | "@vitejs/plugin-react": "^4.3.1", 32 | "eslint": "^9.9.0", 33 | "eslint-plugin-react-hooks": "^5.1.0-rc.0", 34 | "eslint-plugin-react-refresh": "^0.4.9", 35 | "globals": "^15.9.0", 36 | "typescript": "^5.5.3", 37 | "typescript-eslint": "^8.0.1", 38 | "vite": "^5.4.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /server/controllers/connectDatabaseController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction, RequestHandler } from 'express'; 2 | // import connectToDatabase from '../models/dbConnection'; 3 | import mongoose from 'mongoose'; 4 | import dotenv from 'dotenv'; 5 | 6 | 7 | interface connectDatabaseController { 8 | connectDatabase: RequestHandler; 9 | } 10 | 11 | export const connectDatabaseController: connectDatabaseController = { 12 | connectDatabase: async ( 13 | _req: Request, 14 | res: Response, 15 | next: NextFunction 16 | ): Promise => { 17 | try{ 18 | dotenv.config(); 19 | // await mongoose.connection.close(); 20 | // mongoose.disconnect(); 21 | // console.log('MONGODB_URI from .env is: ', process.env.MONGODB_URI); 22 | await mongoose.connect(process.env.MONGODB_URI as string); 23 | console.log('Connected to MongoDB'); 24 | return next(); 25 | } catch (err) { 26 | return next({ 27 | log: 'Error in connectDatabaseController', 28 | status: 500, 29 | message: { err: err+'Error occurred when connecting to MongoDB' }, 30 | }); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /client/src/components/PercentileLatencyComponent.tsx: -------------------------------------------------------------------------------- 1 | import { Bar } from 'react-chartjs-2'; 2 | 3 | interface PercentileData { 4 | p90: number[]; 5 | p95: number[]; 6 | p99: number[]; 7 | } 8 | 9 | interface Props { 10 | data: PercentileData; 11 | } 12 | 13 | const PercentileLatencyComponent = ({ data }: Props) => { 14 | const colorPalette = [ 15 | '#4c88a1', 16 | '#79abc0', 17 | '#adccd8', 18 | ]; 19 | 20 | const chartData = { 21 | labels: ['Percentiles'], 22 | datasets: (Object.keys(data) as Array).map((key, index) => ({ 23 | label: `${key.toUpperCase()} Latency (ms)`, 24 | data: [data[key][0]], 25 | backgroundColor: colorPalette[index], 26 | borderRadius: 4 27 | })), 28 | } 29 | 30 | const options = { 31 | indexAxis: 'y', 32 | scales: { 33 | x: { 34 | title: { 35 | display: true, 36 | text: 'Latency (ms)', 37 | }, 38 | grid: { 39 | display: false 40 | } 41 | } 42 | }, 43 | }; 44 | 45 | return ( 46 |
47 |

Percentile Latency (ms)

48 | 49 |
50 | ) 51 | 52 | } 53 | 54 | export default PercentileLatencyComponent -------------------------------------------------------------------------------- /client/src/containers/ColdStartsMetricsContainer.tsx: -------------------------------------------------------------------------------- 1 | import RowComponent from "../components/RowComponent"; 2 | 3 | interface FunctionData { 4 | functionName: string; 5 | avgBilledDur: number; 6 | numColdStarts: number; 7 | percentColdStarts: number; 8 | } 9 | 10 | interface Props { 11 | data: FunctionData[]; 12 | } 13 | 14 | const ColdStartsMetricsContainer = ({ data }: Props) => { 15 | 16 | return ( 17 |
18 |

Cold Start Performance Metrics

19 |
20 |
21 |
Function Name
22 |
Average Billed Duration
23 |
# Cold Starts
24 |
% Cold Starts
25 |
26 |
27 | {data.map((row, index) => ( 28 | 35 | ))} 36 |
37 |
38 |
39 | ); 40 | }; 41 | 42 | export default ColdStartsMetricsContainer; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LAMBDA LENS 2 | 3 | We built Lambda Lens to provide users a more intuitive UI experience than running logs through AWS' CLI. Lambda Lens aggregates and tracks key performance metrics like Cold Starts for users to evaluate their Lambda Functions. 4 | 5 | Thank you for using our app! 6 | 7 | # USER REQUIREMENTS 8 | 9 | 1. An **AWS Account** with admin access 10 | 2. A **MongoDB URI** 11 | 12 | If you don't have a MongoDB account you may register at https://www.mongodb.com/cloud/atlas/register 13 | 14 | ## Env variables 15 | 16 | Users may configure these variables directly on the app's Configuration page, however, users are also free to hard code the following keys into a .env file manually. 17 | 18 | ``` 19 | AWS_ACCESS_KEY_ID= 20 | AWS_SECRET_ACCESS_KEY= 21 | AWS_REGION= 22 | MONGODB_URI= 23 | ``` 24 | 25 | # RUNNING THE APPLICATION 26 | 27 | ## Installing dependencies 28 | 29 | ``` 30 | cd client || cd server 31 | npm install 32 | ``` 33 | 34 | ## Running the servers 35 | 36 | ``` 37 | cd client || cd server 38 | npm run dev 39 | ``` 40 | 41 | - client will be running on: http://localhost:3000/ 42 | - server will be running on: http://localhost:8080/ 43 | 44 | ## Compiling TypeScript (before deployment) 45 | 46 | ``` 47 | cd client || cd server 48 | npm run build 49 | ``` 50 | -------------------------------------------------------------------------------- /client/src/NavbarComponent.css: -------------------------------------------------------------------------------- 1 | .navbar { 2 | display: flex; 3 | align-items: center; 4 | background-color: #333; 5 | padding: 10px 20px; 6 | position: relative; 7 | } 8 | 9 | #navbarLinks { 10 | list-style-type: none; 11 | margin: 0; 12 | padding: 0; 13 | display: flex; 14 | gap: 20px; 15 | } 16 | 17 | #navbarLinks li { 18 | margin: 0 15px; 19 | } 20 | 21 | #navbarLinks a { 22 | color: #fff; 23 | text-decoration: none; 24 | padding: 10px; 25 | } 26 | 27 | #navbarLinks a:hover { 28 | text-decoration: underline; 29 | } 30 | 31 | #theme-switch { 32 | height: 30px; 33 | width: 30px; 34 | padding: 7px; 35 | border-radius: 50%; 36 | background-color: var(--base-variant); 37 | display: flex; 38 | justify-content: center; 39 | align-items: center; 40 | cursor: pointer; 41 | } 42 | 43 | .darkmode .navbar { 44 | background-color: #222; 45 | } 46 | 47 | .darkmode #navbarLinks a { 48 | color: #fff; 49 | } 50 | 51 | .darkmode .logo-container img { 52 | filter: brightness(0.8); 53 | transition: filter 0.3s ease; 54 | } 55 | 56 | @media (max-width: 768px) { 57 | .logo-container { 58 | width: 15%; 59 | max-width: 80px; 60 | } 61 | } 62 | 63 | @media (max-width: 480px) { 64 | .logo-container { 65 | width: 20%; 66 | max-width: 60px; 67 | } 68 | } -------------------------------------------------------------------------------- /client/src/components/ColdStartsGraphComponent.tsx: -------------------------------------------------------------------------------- 1 | import { Doughnut } from 'react-chartjs-2'; 2 | 3 | interface FunctionData { 4 | functionName: string; 5 | numColdStarts: number; 6 | } 7 | 8 | interface Props { 9 | data: FunctionData[]; 10 | } 11 | 12 | const ColdStartsGraphComponent = ({ data }: Props) => { 13 | const chartData = { 14 | labels: data.map((fn) => fn.functionName), 15 | datasets: [ 16 | { 17 | data: data.map((fn) => fn.numColdStarts), 18 | backgroundColor: [ 19 | '#437990', 20 | '#4c88a1', 21 | '#5796af', 22 | '#68a0b7', 23 | '#79abc0', 24 | '#8bb6c8', 25 | '#9cc1d0', 26 | '#adccd8', 27 | '#bfd7e0', 28 | '#d0e1e9', 29 | ], 30 | }, 31 | ], 32 | }; 33 | 34 | const options = { 35 | plugins: { 36 | legend: { 37 | display: true, 38 | position: 'left' as const, 39 | labels: { 40 | color: '#A2A2A2', 41 | }, 42 | }, 43 | }, 44 | }; 45 | 46 | return ( 47 |
48 |

Total Cold Starts

49 | 50 | 57 |
58 | ); 59 | }; 60 | 61 | export default ColdStartsGraphComponent; 62 | -------------------------------------------------------------------------------- /server/server.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response, NextFunction } from 'express'; 2 | import bodyParser from 'body-parser'; 3 | import cors from 'cors'; 4 | import dotenv from 'dotenv'; 5 | import configRoutes from './routes/configRoutes'; 6 | import dataRoutes from './routes/dataRoutes'; 7 | 8 | dotenv.config(); 9 | 10 | const app = express(); 11 | 12 | const PORT = process.env.PORT || 8080; 13 | 14 | app.use(bodyParser.json()); 15 | app.use(cors()); 16 | app.use(express.json()); 17 | 18 | app.use( 19 | '/api/config', 20 | configRoutes, 21 | (_req: Request, res: Response, _next: NextFunction) => { 22 | return res.status(200).json(res.locals.saved); 23 | } 24 | ); 25 | 26 | app.use('/data', dataRoutes); 27 | 28 | app.get('/', (req: Request, res: Response) => { 29 | res.send('Hello'); 30 | }); 31 | 32 | app.use((_req: Request, res: Response) => {return res.status(404).send('This is not the page you\'re looking for')}); 33 | 34 | app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => { 35 | const defaultErr: { 36 | log: string; 37 | status: number; 38 | message: { err: string }; 39 | } = { 40 | log: 'Express error handler caught unknown middleware error', 41 | status: 500, 42 | message: { err: 'An error occurred' }, 43 | }; 44 | const errorObj = Object.assign({}, defaultErr, err); 45 | return res.status(errorObj.status).json(errorObj.message); 46 | }); 47 | 48 | app.listen(PORT, () => { 49 | console.log(`Server running on port ${PORT}`); 50 | }); 51 | -------------------------------------------------------------------------------- /server/routes/dataRoutes.ts: -------------------------------------------------------------------------------- 1 | import { Router, Request, Response, NextFunction } from 'express'; 2 | import { databaseController } from '../controllers/databaseController'; 3 | import { getMetricData } from '../controllers/cloudWatchController'; 4 | import lambdaController from '../controllers/rawDataController'; 5 | import { handleChat } from '../controllers/ChatController'; 6 | import metricsController from '../controllers/percentileController'; 7 | 8 | const dataRouter = Router(); 9 | 10 | dataRouter.get( 11 | '/update', 12 | lambdaController.processLogs, 13 | databaseController.processData, 14 | (_req: Request, res: Response, next: NextFunction) => { 15 | return res.status(200).send(res.locals.allData); 16 | } 17 | ); 18 | 19 | dataRouter.get( 20 | '/req', 21 | lambdaController.processLogs, 22 | databaseController.processData, 23 | databaseController.getProccessedData, 24 | (req: Request, res: Response, next: NextFunction) => { 25 | return res.status(200).send(res.locals.data); 26 | } 27 | ); 28 | 29 | dataRouter.get( 30 | '/cloud', 31 | getMetricData, 32 | (req: Request, res: Response, next: NextFunction) => { 33 | return res.status(200).send(res.locals.cloudData); 34 | } 35 | ); 36 | 37 | dataRouter.get( 38 | '/metrics', 39 | metricsController.processMetrics, 40 | (req: Request, res: Response, next: NextFunction) => { 41 | // console.log('Metric data:', res.locals.metricData); 42 | return res.status(200).json(res.locals.metricData); 43 | } 44 | ); 45 | 46 | dataRouter.post('/chat', handleChat); 47 | 48 | export default dataRouter; 49 | -------------------------------------------------------------------------------- /client/src/components/AvgBilledDurGraphComponent.tsx: -------------------------------------------------------------------------------- 1 | import { Bar } from 'react-chartjs-2'; 2 | 3 | interface Props { 4 | data: { functionName: string; avgBilledDur: number}[]; 5 | } 6 | 7 | const AvgBilledDurGraph = ({ data }: Props) => { 8 | const chartData = { 9 | labels: data.map(fn => fn.functionName), 10 | datasets: [ 11 | { 12 | label: 'Average Billed Duration (ms)', 13 | data: data.map(fn => fn.avgBilledDur), 14 | backgroundColor: '#447A90', 15 | borderRadius: 2, 16 | hoverBackgroundColor: '#62ACCC' 17 | }, 18 | ], 19 | }; 20 | 21 | const options = { 22 | indexAxis: 'y' as const, 23 | scales: { 24 | x: { 25 | grid: { 26 | display: false, 27 | }, 28 | ticks: { 29 | color: '#A2A2A2', 30 | display: true, 31 | }, 32 | title: { 33 | display: true, 34 | text: 'Milliseconds', 35 | color: '#A2A2A2', 36 | }, 37 | }, 38 | y: { 39 | grid: { 40 | display: false, 41 | }, 42 | ticks: { 43 | padding: 10, 44 | color: '#A2A2A2', 45 | }, 46 | title: { 47 | display: true, 48 | text: 'Function Name', 49 | color: '#A2A2A2', 50 | }, 51 | }, 52 | }, 53 | plugins: { 54 | legend: { 55 | display: false, 56 | }, 57 | }, 58 | }; 59 | 60 | return ( 61 |
62 |

Average Billed Duration (ms)

63 |
64 | 65 |
66 |
67 | ) 68 | }; 69 | 70 | export default AvgBilledDurGraph; -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | export default tseslint.config({ 18 | languageOptions: { 19 | // other options... 20 | parserOptions: { 21 | project: ['./tsconfig.node.json', './tsconfig.app.json'], 22 | tsconfigRootDir: import.meta.dirname, 23 | }, 24 | }, 25 | }) 26 | ``` 27 | 28 | - Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` 29 | - Optionally add `...tseslint.configs.stylisticTypeChecked` 30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: 31 | 32 | ```js 33 | // eslint.config.js 34 | import react from 'eslint-plugin-react' 35 | 36 | export default tseslint.config({ 37 | // Set the react version 38 | settings: { react: { version: '18.3' } }, 39 | plugins: { 40 | // Add the react plugin 41 | react, 42 | }, 43 | rules: { 44 | // other rules... 45 | // Enable its recommended rules 46 | ...react.configs.recommended.rules, 47 | ...react.configs['jsx-runtime'].rules, 48 | }, 49 | }) 50 | ``` 51 | -------------------------------------------------------------------------------- /client/src/components/TotalDurationComponent.tsx: -------------------------------------------------------------------------------- 1 | import { Doughnut } from 'react-chartjs-2'; 2 | 3 | interface Props { 4 | data: { 5 | duration: number[]; 6 | timestamps: string[]; 7 | }; 8 | } 9 | 10 | const formatTimestamp = (timestamp: string) => { 11 | const date = new Date(timestamp); 12 | 13 | const formattedDate = date.toLocaleDateString([], { 14 | year: '2-digit', 15 | month: '2-digit', 16 | day: '2-digit', 17 | }); 18 | 19 | const formattedTime = date.toLocaleTimeString([], { 20 | hour: '2-digit', 21 | minute: '2-digit', 22 | }); 23 | 24 | return `${formattedDate} ${formattedTime}`; 25 | }; 26 | 27 | const TotalDurationComponent = ({ data }: Props) => { 28 | const labels = data.timestamps.map(formatTimestamp); 29 | 30 | const chartData = { 31 | labels, 32 | datasets: [ 33 | { 34 | data: data.duration, 35 | backgroundColor: [ 36 | '#437990', 37 | '#4c88a1', 38 | '#5796af', 39 | '#68a0b7', 40 | '#79abc0', 41 | '#8bb6c8', 42 | '#9cc1d0', 43 | '#adccd8', 44 | '#bfd7e0', 45 | '#d0e1e9', 46 | ], 47 | }, 48 | ], 49 | }; 50 | 51 | const options = { 52 | plugins: { 53 | legend: { 54 | display: true, 55 | position: 'left' as const, 56 | labels: { 57 | boxWidth: 20, 58 | padding: 10, 59 | }, 60 | }, 61 | }, 62 | }; 63 | 64 | return ( 65 |
66 |

Average Execution Duration (5min period)

67 | 74 |
75 | ); 76 | }; 77 | 78 | export default TotalDurationComponent; 79 | -------------------------------------------------------------------------------- /client/src/components/ThrottleComponent.tsx: -------------------------------------------------------------------------------- 1 | import { Line } from 'react-chartjs-2'; 2 | 3 | interface Props { 4 | data: { 5 | throttles: number[]; 6 | timestamps: string[]; 7 | }; 8 | } 9 | 10 | const formatTimestamp = (timestamp: string) => { 11 | const date = new Date(timestamp); 12 | 13 | const formattedDate = date.toLocaleDateString([], { 14 | year: '2-digit', 15 | month: '2-digit', 16 | day: '2-digit', 17 | }); 18 | 19 | const formattedTime = date.toLocaleTimeString([], { 20 | hour: '2-digit', 21 | minute: '2-digit' 22 | }); 23 | 24 | return `${formattedDate} ${formattedTime}`; 25 | }; 26 | 27 | const ThrottleComponent = ({ data }: Props) => { 28 | const labels = data.timestamps.map(formatTimestamp); 29 | 30 | const chartData = { 31 | labels, 32 | datasets: [ 33 | { 34 | label: 'Throttles', 35 | data: data.throttles, 36 | borderColor: [ 37 | '#437990', 38 | ], 39 | fill: false, 40 | }, 41 | ], 42 | }; 43 | 44 | const options = { 45 | scales: { 46 | x: { 47 | title: { 48 | display: true, 49 | text: 'End time' 50 | }, 51 | grid: { 52 | display: false 53 | } 54 | }, 55 | y: { 56 | beginAtZero: true, 57 | title: { 58 | display: true, 59 | text: 'Throttles' 60 | }, 61 | grid: { 62 | display: false 63 | } 64 | }, 65 | }, 66 | plugins: { 67 | legend: { 68 | display: false, 69 | }, 70 | }, 71 | }; 72 | 73 | return ( 74 |
75 |

Total Number of Throttles (5min period)

76 | 77 |
78 | ) 79 | }; 80 | 81 | export default ThrottleComponent; -------------------------------------------------------------------------------- /client/src/containers/ConfigPageContainer.tsx: -------------------------------------------------------------------------------- 1 | import ConfigForm from '../components/ConfigPageComponent'; 2 | import './ConfigPageContainer.scss'; 3 | 4 | type Config = { 5 | awsAccessKeyID: string; 6 | awsSecretAccessKey: string; 7 | awsRegion: string; 8 | mongoURI: string; 9 | }; 10 | 11 | function ConfigPageContainer() { 12 | const handleSaveConfig = (config: Required) => { 13 | fetch('http://localhost:8080/api/config/save', { 14 | method: 'POST', 15 | headers: { 16 | 'Content-Type': 'application/json', 17 | }, 18 | body: JSON.stringify(config), 19 | }) 20 | .then((res) => { 21 | if (res.ok) { 22 | alert(`Configuration saved`); 23 | } else { 24 | alert('Error saving user information'); 25 | } 26 | }) 27 | .catch((err) => { 28 | console.log('The following error occurred:', err); 29 | }); 30 | }; 31 | 32 | const handleSaveDatabase = () => { 33 | fetch('http://localhost:8080/api/config/db', { 34 | method: 'GET', 35 | headers: { 36 | 'Content-Type': 'application/json', 37 | }, 38 | }) 39 | .then((res) => { 40 | if (res.ok) { 41 | window.location.replace('http://localhost:3000/dash'); 42 | } else { 43 | alert( 44 | 'Error connecting to database. Please check for valid URI input' 45 | ); 46 | } 47 | }) 48 | .catch((err) => { 49 | console.log('Error in handleDatabase: ', err); 50 | }); 51 | }; 52 | return ( 53 |
54 |

Configuration

55 |
56 | 57 |
58 |
59 | ); 60 | } 61 | 62 | export default ConfigPageContainer; 63 | -------------------------------------------------------------------------------- /client/src/components/ConcurrExecComponent.tsx: -------------------------------------------------------------------------------- 1 | import { Bar } from "react-chartjs-2"; 2 | 3 | interface Props { 4 | data: { 5 | concurrentExecutions: number[]; 6 | timestamps: string[]; 7 | }; 8 | } 9 | 10 | const formatTimestamp = (timestamp: string) => { 11 | const date = new Date(timestamp); 12 | 13 | const formattedDate = date.toLocaleDateString([], { 14 | year: '2-digit', 15 | month: '2-digit', 16 | day: '2-digit', 17 | }); 18 | 19 | const formattedTime = date.toLocaleTimeString([], { 20 | hour: '2-digit', 21 | minute: '2-digit' 22 | }); 23 | 24 | return `${formattedDate} ${formattedTime}`; 25 | }; 26 | 27 | const ConcurrExecComponent = ({ data }: Props) => { 28 | const labels = data.timestamps.map(formatTimestamp); 29 | 30 | const chartData = { 31 | labels, 32 | datasets: [ 33 | { 34 | label: 'Executions', 35 | data: data.concurrentExecutions, 36 | backgroundColor: [ 37 | '#437990', 38 | '#4c88a1', 39 | '#5796af', 40 | '#68a0b7', 41 | '#79abc0', 42 | '#8bb6c8', 43 | '#9cc1d0', 44 | '#adccd8', 45 | '#bfd7e0', 46 | '#d0e1e9' 47 | ], 48 | borderRadius: 4 49 | }, 50 | ], 51 | }; 52 | 53 | const options = { 54 | indexAxis: 'y' as const, 55 | scales: { 56 | x: { 57 | title: { 58 | display: true, 59 | text: 'Executions' 60 | }, 61 | grid: { 62 | display: false 63 | } 64 | }, 65 | y: { 66 | beginAtZero: true, 67 | title: { 68 | display: true, 69 | text: 'End time' 70 | }, 71 | grid: { 72 | display: false 73 | } 74 | }, 75 | }, 76 | plugins: { 77 | legend: { 78 | display: false, 79 | }, 80 | }, 81 | }; 82 | 83 | return ( 84 |
85 |

Total Concurrent Executions (5min period)

86 | 87 |
88 | ) 89 | }; 90 | 91 | export default ConcurrExecComponent; -------------------------------------------------------------------------------- /client/src/components/ConfigPageComponent.tsx: -------------------------------------------------------------------------------- 1 | import './ConfigPageComponent.scss'; 2 | import * as React from 'react'; 3 | import { useForm, SubmitHandler } from 'react-hook-form'; 4 | 5 | type Config = { 6 | awsAccessKeyID: string; 7 | awsSecretAccessKey: string; 8 | awsRegion: string; 9 | mongoURI: string; 10 | }; 11 | 12 | type ConfigFormProps = { 13 | onSave: (config: Config) => void; 14 | onDatabase: () => void; 15 | }; 16 | 17 | function ConfigForm({ onSave, onDatabase }: ConfigFormProps) { 18 | const { 19 | register, 20 | handleSubmit, 21 | formState: { errors }, 22 | } = useForm(); 23 | const onSubmit: SubmitHandler = (data) => { 24 | onSave(data); 25 | }; 26 | 27 | const handleDatabase = (e: React.FormEvent) => { 28 | e.preventDefault(); 29 | onDatabase(); 30 | }; 31 | 32 | return ( 33 |
34 | 38 | {errors.awsAccessKeyID && ( 39 |

AWS Access Key ID is required

40 | )} 41 | 42 | 46 | {errors.awsSecretAccessKey && ( 47 |

AWS Secret Access Key is required

48 | )} 49 | 50 | 56 | {errors.awsRegion && ( 57 |

AWS Region is required

58 | )} 59 | 60 | 64 | {errors.mongoURI && ( 65 |

MongoDB URI is required

66 | )} 67 | 68 | 69 | 72 |
73 | ); 74 | } 75 | 76 | export default ConfigForm; 77 | -------------------------------------------------------------------------------- /server/controllers/envController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction, RequestHandler } from 'express'; 2 | import fs from 'fs'; 3 | import dotenv from 'dotenv'; 4 | import mongoose from 'mongoose'; 5 | 6 | interface EnvController { 7 | saveSecrets: RequestHandler; 8 | } 9 | 10 | export const envController: EnvController = { 11 | //save secrets (credentials, region, and MongoDB URI) into .env 12 | saveSecrets: async ( 13 | req: Request, 14 | res: Response, 15 | next: NextFunction 16 | ): Promise => { 17 | const { 18 | awsAccessKeyID, 19 | awsSecretAccessKey, 20 | awsRegion, 21 | mongoURI, 22 | }: { 23 | awsAccessKeyID: string; 24 | awsSecretAccessKey: string; 25 | awsRegion: string; 26 | mongoURI: string; 27 | } = req.body; 28 | //Format .env: 29 | const writeToENV = 30 | `AWS_ACCESS_KEY_ID=${awsAccessKeyID}\n` + 31 | `AWS_SECRET_ACCESS_KEY=${awsSecretAccessKey}\n` + 32 | `AWS_REGION=${awsRegion}\n` + 33 | `MONGODB_URI=${mongoURI}\n`; 34 | 35 | try { 36 | //if everything exists in the req body --> write env file 37 | if ( 38 | !req.body.awsAccessKeyID || 39 | !req.body.awsRegion || 40 | !req.body.awsSecretAccessKey || 41 | !req.body.mongoURI 42 | ) { 43 | return next({ 44 | log: `Error in envController.saveSecrets`, //more semantic (fields missing) 45 | status: 500, 46 | message: { err: 'One or more fields missing.' }, //more semantic (fields missing) 47 | }); 48 | } 49 | //else if at least one field is missing --> return an error; 50 | // console.log('Made it to the try block'); 51 | fs.writeFileSync('./.env', writeToENV); 52 | res.locals.saved = 'Secrets successfuly saved'; 53 | console.log('Saved to .env'); 54 | dotenv.config(); 55 | 56 | // should close database connection so it can be reconnected when user presses "connect to db" button 57 | // await mongoose.connection.close(); 58 | // mongoose.disconnect(); 59 | return next(); 60 | } catch (err) { 61 | return next({ 62 | log: `${err}: Error caught in envController.saveSecrets middleware function.`, 63 | status: 500, 64 | message: { 65 | err: 'Error saving AWS Access Key ID, AWS Secret Access Key, AWS region, and MongoURI.', 66 | }, 67 | }); 68 | } 69 | }, 70 | }; 71 | -------------------------------------------------------------------------------- /server/controllers/databaseController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import visData from '../models/visDataModel'; 3 | import { getAwsConfig } from '../configs/awsconfig'; 4 | 5 | 6 | interface Log { 7 | Date: string; 8 | Time: string; 9 | FunctionName: string; 10 | BilledDuration: string; 11 | InitDuration?: string; 12 | MaxMemUsed: string; 13 | } 14 | 15 | interface RawData { 16 | functionName: string; 17 | logs: Log[]; 18 | } 19 | 20 | export const databaseController = { 21 | processData: async ( 22 | _req: Request, 23 | res: Response, 24 | next: NextFunction 25 | ): Promise => { 26 | try { 27 | const awsconfig = getAwsConfig(); 28 | const { region } = awsconfig; 29 | 30 | const rawData: RawData[] = res.locals.allData; 31 | 32 | for (let func of rawData) { 33 | let totalStarts = func.logs.length + 1; 34 | let billed = 0; 35 | let cold = 0; 36 | 37 | for (const log of func.logs) { 38 | billed += parseInt(log.BilledDuration, 10); 39 | if (log.InitDuration) cold++; 40 | } 41 | 42 | const percentCold = totalStarts > 0 ? (cold / totalStarts) * 100 : 0; 43 | 44 | await visData.findOneAndUpdate( 45 | { functionName: func.functionName, region: region }, 46 | { 47 | region: region, 48 | functionName: func.functionName, 49 | avgBilledDur: billed, 50 | numColdStarts: cold, 51 | percentColdStarts: percentCold.toFixed(2), 52 | }, 53 | { 54 | upsert: true, 55 | returnNewDocument: true, 56 | } 57 | ); 58 | } 59 | return next(); 60 | } catch (err) { 61 | next({ 62 | log: 'Error in databaseController.processData', 63 | status: 500, 64 | message: { err: 'Error occurred when finding/updating database.' }, 65 | }); 66 | } 67 | }, 68 | 69 | getProccessedData: async ( 70 | _req: Request, 71 | res: Response, 72 | next: NextFunction 73 | ): Promise => { 74 | try { 75 | const awsconfig = getAwsConfig(); 76 | const { region } = awsconfig; 77 | 78 | res.locals.data = await visData.find({ region }); 79 | return next(); 80 | } catch (err) { 81 | next({ 82 | log: 'Error in databaseController.getProcessedData', 83 | status: 500, 84 | message: { err: 'Error occurred when finding from database' }, 85 | }); 86 | } 87 | }, 88 | }; 89 | -------------------------------------------------------------------------------- /client/src/containers/DashboardContainer.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import ColdStartsGraphComponent from '../components/ColdStartsGraphComponent'; 3 | import ColdStartsMetricsContainer from './ColdStartsMetricsContainer'; 4 | import AvgBilledDurGraph from '../components/AvgBilledDurGraphComponent'; 5 | import ChatContainer from './ChatContainer'; 6 | import '../Graphs.css'; 7 | 8 | interface FunctionData { 9 | functionName: string; 10 | avgBilledDur: number; 11 | numColdStarts: number; 12 | percentColdStarts: number; 13 | } 14 | 15 | const DashboardContainer = () => { 16 | const [data, setData] = useState([]); 17 | const [isClicked, setClicked] = useState(false); 18 | 19 | const fetchData = () => { 20 | fetch('http://localhost:8080/data/req') 21 | .then((res) => res.json()) 22 | .then((data) => setData(data)) 23 | .catch((err) => { 24 | console.log(err); 25 | }); 26 | }; 27 | 28 | useEffect(() => { 29 | fetchData(); 30 | }, []); 31 | 32 | const handleRefresh = () => { 33 | setClicked(true); 34 | fetchData(); 35 | setTimeout(() => setClicked(false), 1000); 36 | }; 37 | 38 | const sortedData = data 39 | .sort((a, b) => b.percentColdStarts - a.percentColdStarts) 40 | .slice(0, 5); 41 | 42 | return ( 43 |
44 | {/* Quadrant 1 */} 45 | {/*
*/} 46 |
47 |

Function Performance

48 | 54 |
55 |
56 | {/*
*/} 57 |
58 | 59 |
60 | {/*
*/} 61 | 62 | {/* Quadrant 2 */} 63 |
64 | 65 |
66 | 67 | {/* Quadrant 3 */} 68 |
69 | 70 |
71 | 72 | {/* Quadrant 4 */} 73 |
74 |

Bedrock Analysis

75 | 76 |
77 |
78 | {/*
*/} 79 |
80 | ); 81 | }; 82 | 83 | export default DashboardContainer; 84 | -------------------------------------------------------------------------------- /client/src/components/NavbarComponent.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import '../NavbarComponent.css'; 4 | import lambda from '../assets/lambda.png'; 5 | 6 | const LightModeIcon = () => ( 7 | 14 | 15 | 16 | ); 17 | 18 | const DarkModeIcon = () => ( 19 | 26 | 27 | 28 | ); 29 | 30 | const NavbarComponent = () => { 31 | const [darkMode, setDarkMode] = useState(false); 32 | 33 | useEffect(() => { 34 | const savedTheme = localStorage.getItem('theme'); 35 | if (savedTheme === 'dark') { 36 | setDarkMode(true); 37 | document.body.classList.add('darkmode'); 38 | } 39 | }, []); 40 | 41 | const toggleDarkMode = () => { 42 | setDarkMode(!darkMode); 43 | if (!darkMode) { 44 | document.body.classList.add('darkmode'); 45 | localStorage.setItem('theme', 'dark'); 46 | } else { 47 | document.body.classList.remove('darkmode'); 48 | localStorage.removeItem('theme'); 49 | } 50 | }; 51 | 52 | return ( 53 | 74 | ); 75 | }; 76 | 77 | export default NavbarComponent; 78 | -------------------------------------------------------------------------------- /client/src/containers/ChatContainer.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import '../App.css'; 3 | 4 | const ChatContainer = () => { 5 | const [messages, setMessages] = useState<{ 6 | role: 'user' | 'assistant'; 7 | content: string 8 | }[]>([]); 9 | const [input, setInput] = useState(''); 10 | const [loading, setLoading] = useState(false); 11 | 12 | const handleSendMessage = async () => { 13 | if (input.trim() === '') return; 14 | 15 | setMessages(prevMessages => [ 16 | ...prevMessages, 17 | { role: 'user', content: input } 18 | ]); 19 | setInput(''); 20 | setLoading(true); 21 | 22 | try { 23 | const response = await fetch('http://localhost:8080/data/chat', { 24 | method: 'POST', 25 | headers: { 'Content-Type': 'application/json' }, 26 | body: JSON.stringify({ message: input }), 27 | }); 28 | 29 | if (!response.ok) { 30 | throw new Error('Network response was not ok'); 31 | } 32 | 33 | const data = await response.json(); 34 | // console.log('Server response:', data); 35 | 36 | setMessages(prevMessages => [ 37 | ...prevMessages, 38 | // { role: 'user', content: input }, 39 | { role: 'assistant', content: data.result || 'No response' } 40 | ]); 41 | } catch (error) { 42 | console.error('Error sending message:', error); 43 | setMessages(prevMessages => [ 44 | ...prevMessages, 45 | { role: 'assistant', content: 'Assistant went wrong' } 46 | ]); 47 | } finally { 48 | setLoading(false); 49 | } 50 | }; 51 | 52 | return ( 53 |
54 |
Explore Your Metrics: Ask Me How!
55 |
56 | {messages.map((msg, index) => ( 57 |
58 | {msg.role === 'user' ? 'You' : 'Assistant'}: 59 |

{msg.content}

60 |
61 | ))} 62 | {loading &&
Assistant:

...

} 63 |
64 |
65 | setInput(e.target.value)} 69 | placeholder='Type your message here...' 70 | /> 71 | 72 |
73 |
74 | ); 75 | }; 76 | 77 | export default ChatContainer; -------------------------------------------------------------------------------- /client/src/containers/CloudwatchContainer.scss: -------------------------------------------------------------------------------- 1 | $primary-text: #161616; 2 | $secondary-text: #646464; 3 | 4 | $highlight-element: #62accc; 5 | $standard-element: #447a90; 6 | 7 | $largest-container: #ffffff; 8 | $medium-container: #f3f3f3; 9 | $smallest-container: #e1e1e1; 10 | body { 11 | background-color: $largest-container; 12 | margin-left: 2px; 13 | margin-right: 2px; 14 | } 15 | .dashboard-header-cw { 16 | font-family: Helvetica, Arial, sans-serif; 17 | display: flex; 18 | justify-content: flex-start; 19 | align-items: center; 20 | 21 | h1 { 22 | margin-right: 16px; 23 | font-weight: 400; 24 | padding-left: 20px; 25 | font-size: 48px; 26 | } 27 | select { 28 | border-radius: 8px; 29 | padding: 8px; 30 | border: 1px; 31 | background-color: $smallest-container; 32 | color: $primary-text; 33 | } 34 | } 35 | 36 | .grid-container { 37 | display: grid; 38 | grid-template-columns: repeat(2, 1fr); 39 | grid-template-rows: auto auto; 40 | gap: 20px; 41 | padding: 20px; 42 | } 43 | 44 | .component-box-cw { 45 | background-color: $medium-container; 46 | border: 1px solid #ddd; 47 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 48 | padding: 16px; 49 | border-radius: 8px; 50 | flex-grow: 1; 51 | h2 { 52 | display: flex; 53 | justify-content: flex-start; 54 | align-items: center; 55 | font-weight: 400; 56 | padding: 20px; 57 | } 58 | .bar { 59 | background-color: $smallest-container; 60 | border-radius: 8px; 61 | padding: 8px; 62 | box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1), 0px 1px 3px rgba(0, 0, 0, 0.08); 63 | } 64 | .line { 65 | background-color: $smallest-container; 66 | border-radius: 8px; 67 | padding: 8px; 68 | box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1), 0px 1px 3px rgba(0, 0, 0, 0.08); 69 | } 70 | .pie { 71 | background-color: $smallest-container; 72 | border-radius: 8px; 73 | padding: 8px; 74 | max-height: 445px; 75 | box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1), 0px 1px 3px rgba(0, 0, 0, 0.08); 76 | } 77 | } 78 | 79 | body.darkmode { 80 | .dashboard-header-cw { 81 | font-family: Helvetica, Arial, sans-serif; 82 | display: flex; 83 | justify-content: flex-start; 84 | align-items: center; 85 | 86 | h1 { 87 | margin-right: 16px; 88 | font-weight: 400; 89 | } 90 | select { 91 | border-radius: 8px; 92 | padding: 8px; 93 | border: 1px; 94 | background-color: #363636; 95 | color: #a2a2a2; 96 | } 97 | } 98 | 99 | .component-box-cw { 100 | background-color: #2a2a2a; 101 | border-color: #363636; 102 | h2 { 103 | display: flex; 104 | justify-content: flex-start; 105 | align-items: center; 106 | font-weight: 400; 107 | } 108 | .bar { 109 | background-color: #363636; 110 | border-radius: 8px; 111 | padding: 8px; 112 | box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1), 113 | 0px 1px 3px rgba(0, 0, 0, 0.08); 114 | } 115 | .line { 116 | background-color: #363636; 117 | border-radius: 8px; 118 | padding: 8px; 119 | box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1), 120 | 0px 1px 3px rgba(0, 0, 0, 0.08); 121 | } 122 | .pie { 123 | background-color: #363636; 124 | border-radius: 8px; 125 | padding: 8px; 126 | max-height: 445px; 127 | box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1), 128 | 0px 1px 3px rgba(0, 0, 0, 0.08); 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /client/src/Graphs.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Helvetica, Arial, sans-serif; 3 | background-color: #ffffff; 4 | } 5 | 6 | /* Refresh Button */ 7 | 8 | .refresh-button { 9 | border-radius: 50%; 10 | height: 30px; 11 | width: 30px; 12 | cursor: pointer; 13 | border: 1px solid #ccc; 14 | } 15 | 16 | .refresh-button:hover { 17 | background-color: #e0e0e0; 18 | } 19 | 20 | .refresh-button.clicked { 21 | background-color: #62accc; 22 | } 23 | 24 | /* Component Boxes */ 25 | 26 | .grid { 27 | display: grid; /* Use grid layout */ 28 | grid-template-columns: repeat(2, 1fr); /* Two equal columns */ 29 | grid-template-rows: repeat(2, 400px); /* Two equal rows with fixed height */ 30 | gap: 20px; /* Space between boxes */ 31 | padding: 20px; /* Space inside the container */ 32 | box-sizing: border-box; 33 | } 34 | 35 | .component-box { 36 | /* border: 1px solid #ddd; 37 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 38 | padding: 16px; 39 | background-color: #f3f3f3; 40 | border-radius: 8px; 41 | flex-grow: 1; */ 42 | 43 | border: 1px solid #ddd; 44 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 45 | padding: 16px; 46 | background-color: #f3f3f3; 47 | border-radius: 8px; 48 | box-sizing: border-box; 49 | display: flex; 50 | align-items: center; 51 | justify-content: center; 52 | } 53 | 54 | @media (max-width: 600px) { 55 | .component-box-cont { 56 | grid-template-columns: 1fr; /* Single column layout on small screens */ 57 | grid-template-rows: repeat( 58 | auto-fill, 59 | minmax(200px, 1fr) 60 | ); /* Adjust row size for better fit */ 61 | } 62 | } 63 | 64 | .dashboard-header { 65 | display: flex; 66 | align-items: center; 67 | font-weight: 400; 68 | } 69 | 70 | .dashboard-header h1 { 71 | padding-left: 20px; 72 | padding-right: 20px; 73 | font-weight: 400; 74 | font-size: 48px; 75 | } 76 | 77 | /* Grid Layout */ 78 | 79 | /* .grid { 80 | display: grid; 81 | grid-template-columns: 1fr 1fr; 82 | grid-template-rows: auto auto; 83 | gap: 20px; 84 | padding: 20px; 85 | } */ 86 | 87 | .quadrant { 88 | display: flex; 89 | flex-direction: column; 90 | } 91 | 92 | /* Table */ 93 | 94 | h2 { 95 | margin-top: 0; 96 | margin-bottom: 20px; 97 | display: flex; 98 | /* align-items: flex-start; */ 99 | align-self: flex-start; 100 | font-weight: 500; 101 | font-size: 32px; 102 | } 103 | 104 | .table { 105 | width: 100%; 106 | } 107 | 108 | .header-row, 109 | .table-row { 110 | display: grid; 111 | grid-template-columns: 2fr 1fr 1fr 1fr; 112 | gap: 10px; 113 | padding: 12px 16px; 114 | align-items: center; 115 | } 116 | 117 | .table-row { 118 | background-color: #e1e1e1; 119 | border-radius: 8px; 120 | margin-bottom: 10px; 121 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 122 | } 123 | 124 | .header-row { 125 | font-weight: bold; 126 | color: #646464; 127 | } 128 | 129 | .table-body { 130 | max-height: 300px; 131 | overflow-y: auto; 132 | } 133 | 134 | .table-row:hover { 135 | background-color: #f0f0f0; 136 | } 137 | 138 | /* Graph */ 139 | 140 | .chart-wrapper { 141 | background-color: #e1e1e1; 142 | border-radius: 8px; 143 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 144 | padding: 16px; 145 | } 146 | 147 | /* Darkmode */ 148 | 149 | body.darkmode .refresh-button { 150 | background-color: #a2a2a2; 151 | } 152 | 153 | body.darkmode .refresh-button:hover { 154 | background-color: #ffffff; 155 | } 156 | 157 | body.darkmode .refresh-button.clicked { 158 | background-color: #62accc; 159 | } 160 | 161 | body.darkmode .component-box { 162 | background-color: #2a2a2a; 163 | border-color: #363636; 164 | } 165 | 166 | body.darkmode .header-row { 167 | color: #a2a2a2; 168 | } 169 | 170 | body.darkmode .table-row { 171 | color: #ffffff; 172 | background-color: #363636; 173 | } 174 | 175 | body.darkmode .table-row:hover { 176 | background-color: #a2a2a2; 177 | } 178 | 179 | body.darkmode .chart-wrapper { 180 | background-color: #363636; 181 | } 182 | 183 | body.darkmode .table-header { 184 | color: #a2a2a2; 185 | } 186 | -------------------------------------------------------------------------------- /client/src/containers/CloudwatchContainer.tsx: -------------------------------------------------------------------------------- 1 | import ConcurrExecComponent from "../components/ConcurrExecComponent"; 2 | import ThrottleComponent from "../components/ThrottleComponent"; 3 | import TotalDurationComponent from "../components/TotalDurationComponent"; 4 | import PercentileLatencyComponent from "../components/PercentileLatencyComponent"; 5 | import { useState, useEffect } from "react"; 6 | import './CloudwatchContainer.scss'; 7 | 8 | interface FunctionData { 9 | functionName: string; 10 | duration: number[]; 11 | concurrentExecutions: number[]; 12 | throttles: number[]; 13 | timestamps: string[]; 14 | } 15 | 16 | interface PercentileData { 17 | p90: number[]; 18 | p95: number[]; 19 | p99: number[]; 20 | } 21 | 22 | const CloudwatchContainer = () => { 23 | const [functionData, setFunctionData] = useState([]); 24 | const [selectedFunction, setSelectedFunction] = useState(''); 25 | const [filteredData, setFilteredData] = useState(null); 26 | const [percentileData, setPercentileData] = useState<{ [key: string]: { percentiles: PercentileData } }>({}); 27 | const [filteredPercentileData, setFilteredPercentileData] = useState(null); 28 | 29 | useEffect(() => { 30 | fetch('http://localhost:8080/data/cloud') 31 | .then(res => res.json()) 32 | .then((data: FunctionData[]) => { 33 | setFunctionData(data); 34 | if (data.length > 0) { 35 | setSelectedFunction(data[0].functionName); 36 | } 37 | }) 38 | .catch(err => console.log(err)); 39 | }, []); 40 | 41 | useEffect(() => { 42 | fetch('http://localhost:8080/data/metrics') 43 | .then(res => res.json()) 44 | .then((data: { [key: string]: { percentiles: PercentileData } }) => { 45 | setPercentileData(data); 46 | }) 47 | .catch(err => console.log(err)); 48 | }, []); 49 | 50 | useEffect(() => { 51 | if (selectedFunction && functionData.length > 0) { 52 | const selected = functionData.find(func => func.functionName === selectedFunction); 53 | setFilteredData(selected || null); 54 | 55 | const selectedPercentile = percentileData[selectedFunction]?.percentiles; 56 | setFilteredPercentileData(selectedPercentile || null); 57 | } 58 | }, [selectedFunction, functionData, percentileData]); 59 | 60 | return ( 61 |
62 |
63 |

CloudWatch Metrics

64 | 74 |
75 |
76 | {filteredData && ( 77 | <> 78 |
79 | 80 |
81 |
82 | 83 |
84 |
85 | 86 |
87 | 88 | )} 89 | {filteredPercentileData && ( 90 |
91 | 92 |
93 | )} 94 |
95 |
96 | ); 97 | } 98 | 99 | export default CloudwatchContainer; -------------------------------------------------------------------------------- /server/controllers/percentileController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import { CloudWatchClient, GetMetricDataCommand, GetMetricDataCommandOutput } from '@aws-sdk/client-cloudwatch'; 3 | import { getFunction } from './getFunctionsController'; 4 | import { getAwsConfig } from '../configs/awsconfig'; 5 | 6 | 7 | interface MetricData { 8 | [functionName: string]: { 9 | percentiles: Record; 10 | }; 11 | } 12 | 13 | // Define percentile we want 14 | const percentiles = ['p90', 'p95', 'p99']; 15 | 16 | // create unqiue id for each metric query 17 | const generateValidId = (prefix: string, index: number, value: string) => { 18 | return `${prefix}_${index}_${value}`.toLowerCase().replace(/[^a-z0-9_]/g, '_'); 19 | }; 20 | 21 | // getting the metrics within the function names (func : related percentile) 22 | const fetchMetrics = async (functionNames: string[]): Promise => { 23 | const queries = functionNames.flatMap((functionName, index) => [ 24 | ...percentiles.map((percentile) => ({ 25 | Id: generateValidId('percentile', index, percentile), 26 | MetricStat: { 27 | Metric: { 28 | Namespace: 'AWS/Lambda', 29 | MetricName: 'Duration', 30 | Dimensions: [{ Name: 'FunctionName', Value: functionName }], 31 | }, 32 | Period: 300, 33 | Stat: percentile, 34 | }, 35 | ReturnData: true, 36 | })), 37 | ]); 38 | 39 | const endTime = new Date(); 40 | const startTime = new Date(endTime.getTime() - 90 * 24 * 60 * 60 * 1000); 41 | 42 | const command = new GetMetricDataCommand({ 43 | MetricDataQueries: queries, 44 | StartTime: startTime, 45 | EndTime: endTime, 46 | }); 47 | 48 | try { 49 | const awsconfig = getAwsConfig(); 50 | const client = new CloudWatchClient(awsconfig); 51 | 52 | const data: GetMetricDataCommandOutput = await client.send(command); 53 | return data; 54 | } catch (error) { 55 | console.error('Error fetching metric data:', error); 56 | throw new Error('Error fetching metric data'); 57 | } 58 | }; 59 | 60 | // Averaging the values from array to get single values for each func 61 | const average = (values: number[]): number => { 62 | const sum = values.reduce((acc, value) => acc + value, 0); 63 | return values.length > 0 ? sum / values.length : 0; 64 | }; 65 | 66 | // process the metric responses from cloudwatch find and aggregate the percentile values 67 | // store it in res.locals.metricData 68 | const metricsController = { 69 | async processMetrics(_req: Request, res: Response, next: NextFunction) { 70 | try { 71 | const functionNames = await getFunction(); 72 | 73 | if (functionNames.length === 0) { 74 | return res.status(404).json({ error: 'No Lambda functions found' }); 75 | } 76 | 77 | const metricData = await fetchMetrics(functionNames); 78 | 79 | // define MetricDataResults 80 | const results = metricData.MetricDataResults ?? []; 81 | 82 | // Process and structure the metric data 83 | const processedData: MetricData = functionNames.reduce((acc, functionName, index) => { 84 | acc[functionName] = { 85 | percentiles: percentiles.reduce((percentileAcc, percentile) => { 86 | const percentileResults = results 87 | .filter(result => result.Id?.includes(generateValidId('percentile', index, percentile))) 88 | .flatMap(result => result.Values ?? []); 89 | 90 | // Aggregate the percentile values 91 | percentileAcc[percentile] = percentileResults.length > 0 ? [average(percentileResults)] : [0]; 92 | return percentileAcc; 93 | }, {} as Record), 94 | }; 95 | return acc; 96 | }, {} as MetricData); 97 | 98 | res.locals.metricData = processedData; 99 | return next(); 100 | } catch (error) { 101 | next({ 102 | log: 'Error in metricsController.processMetrics', 103 | status: 500, 104 | message: { err: 'Error occurred when fetching Lambda metrics' }, 105 | }); 106 | } 107 | }, 108 | }; 109 | 110 | export default metricsController; -------------------------------------------------------------------------------- /client/src/App.css: -------------------------------------------------------------------------------- 1 | /* dark mode */ 2 | /* #root { 3 | max-width: 1280px; 4 | margin: 0 auto; 5 | padding: 2rem; 6 | text-align: center; 7 | transition: background-color 1s, color 1s; 8 | } */ 9 | 10 | /* .logo { 11 | height: 2.5em; 12 | padding: 1.5em; 13 | filter: brightness(0) invert(1); 14 | transition: filter 300ms; 15 | } */ 16 | 17 | /* body.darkmode .logo { 18 | filter: brightness(0) invert(1); 19 | } 20 | 21 | .logo:hover { 22 | filter: drop-shadow(0 0 2em #646cffaa); 23 | } 24 | 25 | .logo.react:hover { 26 | filter: drop-shadow(0 0 2em #61dafbaa); 27 | } */ 28 | 29 | /* @keyframes logo-spin { 30 | from { 31 | transform: rotate(0deg); 32 | } 33 | to { 34 | transform: rotate(360deg); 35 | } 36 | } */ 37 | 38 | /* @media (prefers-reduced-motion: no-preference) { 39 | a:nth-of-type(2) .logo { 40 | animation: logo-spin infinite 20s linear; 41 | } 42 | } */ 43 | 44 | .card { 45 | padding: 2em; 46 | } 47 | 48 | .read-the-docs { 49 | color: #888; 50 | } 51 | 52 | /* .navbar { 53 | position: fixed; 54 | top: 0; 55 | left: 0; 56 | width: 100%; 57 | height: 50px; 58 | background-color: rgb(188, 188, 188); 59 | display: flex; 60 | flex-direction: row; 61 | justify-content: flex-start; 62 | align-items: center; 63 | z-index: 1000; 64 | transition: background-color 0.3s; 65 | } */ 66 | 67 | /* ul { 68 | list-style-type: none; 69 | margin: 0; 70 | padding: 0; 71 | } 72 | 73 | #navbarLinks { 74 | display: flex; 75 | flex-direction: row; 76 | justify-content: space-between; 77 | align-items: center; 78 | padding: 0 34px; 79 | } */ 80 | 81 | /* Dark mode styles */ 82 | body.darkmode { 83 | background-color: #1e1e1e; 84 | color: #e0e0e0; 85 | } 86 | 87 | body.darkmode .navbar { 88 | background-color: #333; 89 | } 90 | 91 | body.darkmode #navbarLinks a { 92 | color: #e0e0e0; 93 | } 94 | 95 | body.darkmode .config-page-container { 96 | background-color: #1e1e1e; 97 | color: #e0e0e0; 98 | } 99 | 100 | /* chat style */ 101 | 102 | .chat-container { 103 | border: 1px solid #ddd; 104 | background-color: #e1e1e1; 105 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 106 | border-radius: 8px; 107 | padding: 16px; 108 | margin-top: 20px; 109 | max-height: 500px; 110 | display: flex; 111 | flex-direction: column; 112 | } 113 | 114 | .chat-title { 115 | text-align: center; 116 | font-size: 18px; 117 | font-weight: bold; 118 | margin-bottom: 16px; 119 | } 120 | 121 | .chat-window { 122 | margin-bottom: 10px; 123 | max-height: 300px; 124 | overflow-y: auto; 125 | } 126 | 127 | .chat-message { 128 | margin-bottom: 8px; 129 | display: flex; 130 | flex-direction: column; 131 | align-items: flex-start; 132 | } 133 | 134 | .chat-message .role { 135 | font-weight: bold; 136 | margin-bottom: 4px; 137 | } 138 | 139 | .chat-input { 140 | display: flex; 141 | gap: 10px; 142 | align-items: center; 143 | } 144 | 145 | .chat-input input { 146 | flex: 1; 147 | padding: 8px; 148 | border: 1px solid #ddd; 149 | border-radius: 4px; 150 | } 151 | 152 | .chat-input button { 153 | padding: 8px 16px; 154 | border: none; 155 | background-color: #447a90; 156 | color: #ffffff; 157 | border-radius: 4px; 158 | cursor: pointer; 159 | } 160 | 161 | .chat-input button:hover { 162 | background-color: #62ACCC; 163 | } 164 | 165 | .chat-message p { 166 | margin: 0; 167 | padding: 5px; 168 | border-radius: 4px; 169 | } 170 | 171 | .chat-message.user p { 172 | background-color: #d1e7dd; 173 | } 174 | 175 | .chat-message.assistant p { 176 | background-color: #e2e3e5; 177 | } 178 | 179 | body.darkmode .chat-container { 180 | background-color: #2b2b2b; 181 | color: #e0e0e0; 182 | border-color: #444; 183 | } 184 | 185 | body.darkmode .chat-message { 186 | background-color: #444; 187 | color: #e0e0e0; 188 | } 189 | 190 | body.darkmode .chat-message.user p { 191 | background-color: #444; 192 | } 193 | 194 | body.darkmode .chat-message.user { 195 | background-color: #2b2b2b; 196 | color: #e0e0e0; 197 | } 198 | 199 | body.darkmode .chat-message.assistant p { 200 | background-color: #444; 201 | } 202 | body.darkmode .chat-message.assistant { 203 | background-color: #2b2b2b; 204 | color: #e0e0e0; 205 | } 206 | 207 | body.darkmode .chat-title { 208 | color: #a2a2a2; 209 | } 210 | -------------------------------------------------------------------------------- /client/src/components/ConfigPageComponent.scss: -------------------------------------------------------------------------------- 1 | // sass variables 2 | $element-h: #62accc; 3 | $element-s: #447a90; 4 | 5 | $light-cont-l: #ffffff; 6 | $light-cont-m: #f3f3f3; 7 | $light-cont-s: #e1e1e1; 8 | $light-text-prim: #161616; 9 | $light-text-sec: #646464; 10 | 11 | $dark-cont-l: #161616; 12 | $dark-cont-m: #2a2a2a; 13 | $dark-cont-s: #363636; 14 | $dark-text-prim: #ffffff; 15 | $dark-text-sec: #a2a2a2; 16 | 17 | $error: #d25d23; 18 | $medium: 550; 19 | 20 | $mui-boxshadow-2: 0px 4px 6px rgba(0, 0, 0, 0.1), 21 | 0px 1px 3px rgba(0, 0, 0, 0.08); 22 | 23 | // light mode settings 24 | .config-form { 25 | display: flex; 26 | flex-direction: column; 27 | background-color: $light-cont-m; 28 | padding: 15px; 29 | padding-top: 25px; 30 | padding-bottom: 25px; 31 | border-radius: 10px; 32 | width: 700px; 33 | max-width: 100%; 34 | font-family: Helvetica, Arial, sans-serif; 35 | box-shadow: $mui-boxshadow-2; 36 | 37 | input, 38 | select { 39 | border: 1px; 40 | border-radius: 4px; 41 | padding: 10px; 42 | margin-bottom: 5px; 43 | color: $light-text-sec; 44 | background-color: $light-cont-s; 45 | font-family: Helvetica, Arial, sans-serif; 46 | outline: none; 47 | } 48 | 49 | select:hover { 50 | border-radius: 4px; 51 | padding: 10px; 52 | margin-bottom: 5px; 53 | color: $light-text-prim; 54 | border: none; 55 | background-color: $light-cont-m; 56 | } 57 | 58 | input:hover { 59 | border: 1px; 60 | border-radius: 4px; 61 | padding: 10px; 62 | margin-bottom: 5px; 63 | color: #161616; 64 | background-color: $light-cont-m; 65 | } 66 | 67 | input:focus { 68 | background-color: $light-cont-m; 69 | border: 2px solid $element-h; 70 | } 71 | 72 | button { 73 | padding: 10px; 74 | border-radius: 4px; 75 | border: none; 76 | } 77 | 78 | .config-submit-button { 79 | background-color: $element-s; 80 | color: $light-cont-l; 81 | font-weight: $medium; 82 | } 83 | .config-submit-button:hover { 84 | background-color: $element-h; 85 | color: $light-cont-l; 86 | } 87 | 88 | .db-button { 89 | background-color: darken($light-cont-s, 60); 90 | color: lighten($light-cont-s, 0); 91 | font-weight: $medium; 92 | } 93 | 94 | .db-button:hover { 95 | background-color: darken($light-cont-s, 55); 96 | color: lighten($light-cont-s, 5); 97 | } 98 | 99 | .config-error { 100 | color: $error; 101 | font-family: Helvetica, Arial, sans-serif; 102 | } 103 | } 104 | 105 | // dark mode settings 106 | body.darkmode .config-form { 107 | display: flex; 108 | flex-direction: column; 109 | background-color: $dark-cont-m; 110 | padding: 15px; 111 | padding-top: 25px; 112 | padding-bottom: 25px; 113 | border-radius: 10px; 114 | width: 700px; 115 | max-width: 100%; 116 | font-family: Helvetica, Arial, sans-serif; 117 | outline: none; 118 | 119 | input, 120 | select { 121 | border: 1px; 122 | border-radius: 4px; 123 | padding: 10px; 124 | margin-bottom: 5px; 125 | color: $dark-text-sec; 126 | background-color: $dark-cont-s; 127 | font-family: Helvetica, Arial, sans-serif; 128 | } 129 | 130 | select:hover { 131 | border-radius: 4px; 132 | padding: 10px; 133 | margin-bottom: 5px; 134 | color: $dark-text-sec; 135 | border: 2px solid $element-h; 136 | background-color: $dark-cont-m; 137 | border: none; 138 | } 139 | 140 | input:hover { 141 | border: 1px; 142 | border-radius: 4px; 143 | padding: 10px; 144 | margin-bottom: 5px; 145 | color: $dark-text-sec; 146 | background-color: $dark-cont-m; 147 | } 148 | 149 | input:focus { 150 | background-color: $dark-cont-m; 151 | border: 2px solid $element-h; 152 | } 153 | 154 | button { 155 | padding: 10px; 156 | border-radius: 4px; 157 | border: none; 158 | } 159 | 160 | .config-submit-button { 161 | background-color: $element-s; 162 | color: $dark-text-prim; 163 | font-weight: $medium; 164 | } 165 | .config-submit-button:hover { 166 | background-color: $element-h; 167 | color: $dark-text-prim; 168 | } 169 | 170 | .db-button { 171 | background-color: $light-cont-s; 172 | color: $light-text-prim; 173 | font-weight: $medium; 174 | } 175 | 176 | .db-button:hover { 177 | background-color: lighten($light-cont-s, 5); 178 | color: $light-text-sec; 179 | } 180 | 181 | .config-error { 182 | color: $error; 183 | font-family: Helvetica, Arial, sans-serif; 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /server/controllers/ChatController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import { BedrockRuntimeClient, InvokeModelCommand, BedrockRuntimeClientConfig } from '@aws-sdk/client-bedrock-runtime'; 3 | import ConversationModel from '../models/ConversationModel'; 4 | import { getAwsConfig } from '../configs/awsconfig'; 5 | 6 | interface ConversationEntry { 7 | role: 'user' | 'assistant'; 8 | content: string; 9 | } 10 | 11 | 12 | 13 | export const handleChat = async (req: Request, res: Response, next: NextFunction) => { 14 | try { 15 | const awsconfig = getAwsConfig(); 16 | 17 | const client = new BedrockRuntimeClient({ 18 | region: process.env.AWS_REGION, 19 | endpoint: 'https://bedrock-runtime.us-east-1.amazonaws.com', 20 | credentials: { 21 | accessKeyId: awsconfig.credentials.accessKeyId, 22 | secretAccessKey: awsconfig.credentials.secretAccessKey, 23 | }, 24 | } as BedrockRuntimeClientConfig); 25 | 26 | const { message } = req.body; 27 | 28 | if (typeof message !== 'string' || message.trim() === '') { 29 | return res.status(400).json({ error: 'Invalid or empty message format' }); 30 | } 31 | 32 | // console.log('Starting request handler'); 33 | 34 | let conversationHistory: ConversationEntry[] = []; 35 | const conversationId = '2'; 36 | 37 | let historyDoc; 38 | try { 39 | historyDoc = await ConversationModel.findOne({ id: conversationId }).exec(); 40 | } catch (dbError) { 41 | return res.status(500).json({ error: `Database query failed: ${(dbError as Error).message}` }); 42 | } 43 | 44 | if (historyDoc) { 45 | try { 46 | conversationHistory = historyDoc.conversation.map(({ role, content }: ConversationEntry) => ({ 47 | role, 48 | content 49 | })); 50 | // console.log('Conversation parsed:', conversationHistory); 51 | } catch (parseError) { 52 | return res.status(500).json({ error: `Failed to parse conversation history: ${(parseError as Error).message}` }); 53 | } 54 | conversationHistory.push({ role: 'user', content: message }); 55 | } else { 56 | // console.log('No conversation found, creating new one'); 57 | conversationHistory = [{ role: 'user', content: message }]; 58 | } 59 | 60 | const input = { 61 | modelId: 'anthropic.claude-3-haiku-20240307-v1:0', 62 | contentType: 'application/json', 63 | accept: 'application/json', 64 | body: JSON.stringify({ 65 | messages: conversationHistory, 66 | max_tokens: 1000, 67 | anthropic_version: 'bedrock-2023-05-31', 68 | }), 69 | }; 70 | 71 | const command = new InvokeModelCommand(input); 72 | let responseBody; 73 | try { 74 | const response = await client.send(command); 75 | 76 | if (response.body instanceof Uint8Array) { 77 | responseBody = new TextDecoder('utf-8').decode(response.body); 78 | } else if (typeof response.body === 'string') { 79 | responseBody = response.body; 80 | } else { 81 | responseBody = (response.body as any).toString(); 82 | } 83 | 84 | const parsedResponse = JSON.parse(responseBody); 85 | // console.log('Bedrock response:', parsedResponse); 86 | 87 | const contentArray = parsedResponse.content; 88 | if (Array.isArray(contentArray) && contentArray.length > 0) { 89 | const chatResponse = contentArray[0]?.text || 'No response'; 90 | // console.log('Chat Response:', chatResponse); 91 | 92 | const updatedConversation: ConversationEntry[] = [ 93 | ...conversationHistory, 94 | { role: 'assistant', content: chatResponse }, 95 | ]; 96 | 97 | try { 98 | await ConversationModel.findOneAndUpdate( 99 | { id: conversationId }, 100 | { conversation: updatedConversation, lastUpdated: new Date() }, 101 | { new: true, upsert: true } 102 | ).exec(); 103 | console.log('Conversation saved to database'); 104 | } catch (dbError) { 105 | return res.status(500).json({ error: `Failed to save conversation to database: ${(dbError as Error).message}` }); 106 | } 107 | 108 | return res.json({ result: chatResponse }); 109 | } else { 110 | return res.status(500).json({ error: 'Invalid content structure in response' }); 111 | } 112 | } catch (error) { 113 | return res.status(500).json({ error: 'Failed to invoke Bedrock model', details: error instanceof Error ? error.message : 'Unknown error' }); 114 | } 115 | } catch (error) { 116 | return res.status(500).json({ error: 'Internal Server Error', details: error instanceof Error ? error.message : 'Unknown error' }); 117 | } 118 | }; 119 | -------------------------------------------------------------------------------- /server/controllers/cloudWatchController.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CloudWatchClient, 3 | GetMetricDataCommand, 4 | GetMetricDataCommandOutput, 5 | } from '@aws-sdk/client-cloudwatch'; 6 | import { Request, Response, NextFunction } from 'express'; 7 | import { getFunction } from './getFunctionsController'; 8 | import { getAwsConfig } from '../configs/awsconfig'; 9 | 10 | 11 | const metricCommand = (funcName: string): GetMetricDataCommand => { 12 | // define time range for the metric data 13 | const endTime = new Date(); 14 | // 90 day period 15 | const startTime = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000); 16 | 17 | return new GetMetricDataCommand({ 18 | MetricDataQueries: [ 19 | // duration metric 20 | { 21 | Id: 'm1', 22 | MetricStat: { 23 | Metric: { 24 | Namespace: 'AWS/Lambda', 25 | MetricName: 'Duration', 26 | Dimensions: [ 27 | { 28 | Name: 'FunctionName', 29 | Value: funcName, 30 | }, 31 | ], 32 | }, 33 | Period: 300, 34 | Stat: 'Average', 35 | }, 36 | ReturnData: true, 37 | }, 38 | // concurrent executions metric 39 | { 40 | Id: 'm2', 41 | MetricStat: { 42 | Metric: { 43 | Namespace: 'AWS/Lambda', 44 | MetricName: 'ConcurrentExecutions', 45 | Dimensions: [ 46 | { 47 | Name: 'FunctionName', 48 | Value: funcName, 49 | }, 50 | ], 51 | }, 52 | Period: 300, 53 | Stat: 'Sum', 54 | }, 55 | ReturnData: true, 56 | }, 57 | // throttle metric 58 | { 59 | Id: 'm3', 60 | MetricStat: { 61 | Metric: { 62 | Namespace: 'AWS/Lambda', 63 | MetricName: 'Throttles', 64 | Dimensions: [ 65 | { 66 | Name: 'FunctionName', 67 | Value: funcName, 68 | }, 69 | ], 70 | }, 71 | Period: 300, 72 | Stat: 'Sum', 73 | }, 74 | ReturnData: true, 75 | }, 76 | ], 77 | StartTime: startTime, 78 | EndTime: endTime, 79 | }); 80 | }; 81 | 82 | // fetch metric data 83 | export const getMetricData = async ( 84 | _req: Request, 85 | res: Response, 86 | next: NextFunction 87 | ): Promise => { 88 | try { 89 | const awsconfig = getAwsConfig(); 90 | 91 | const client = new CloudWatchClient(awsconfig); 92 | // fetch existing AWS Lambda functions 93 | const functionNames = await getFunction(); 94 | 95 | // create an array of commands to be sent to the CloudWatch Client 96 | const commandArr = functionNames.map((functionName) => 97 | metricCommand(functionName) 98 | ); 99 | 100 | // helper function that sends indivual commands to be sent to AWS Client 101 | const fetchData = async ( 102 | command: GetMetricDataCommand 103 | ): Promise => { 104 | return await client.send(command); 105 | }; 106 | 107 | // resolves all promises into a data array 108 | const dataArr = await Promise.all( 109 | commandArr.map((command) => fetchData(command)) 110 | ); 111 | 112 | // map function names to metric reports (functionName array is in same order as dataArr) 113 | const mappedMetricsArray = functionNames.map((functionName, index) => { 114 | const metricData = dataArr[index]; 115 | 116 | // if metric data results don't exist, then return an empty data set 117 | if (!metricData.MetricDataResults) { 118 | throw new Error( 119 | `Metric data for ${functionName} is missing or incomplete.` 120 | ); 121 | return { 122 | functionName, 123 | duration: [], 124 | concurrentExecutions: [], 125 | throttles: [], 126 | timestamps: [], 127 | }; 128 | } 129 | 130 | // if any Values do not exist, set variable to an empty array 131 | const duration = metricData.MetricDataResults[0].Values ?? []; 132 | const concurrent = metricData.MetricDataResults[1].Values ?? []; 133 | const throttles = metricData.MetricDataResults[2].Values ?? []; 134 | const timestamps = metricData.MetricDataResults[0].Timestamps ?? []; 135 | 136 | return { 137 | functionName: functionName, 138 | duration: duration, 139 | concurrentExecutions: concurrent, 140 | throttles: throttles, 141 | timestamps: timestamps, 142 | }; 143 | }); 144 | 145 | res.locals.cloudData = mappedMetricsArray; 146 | return next(); 147 | } catch (error) { 148 | return next({ 149 | log: 'Error in cloudWatchController.getMetricData', 150 | status: 500, 151 | message: { err: 'Error occured when retrieving Cloudwatch Metrics.' }, 152 | }); 153 | } 154 | }; -------------------------------------------------------------------------------- /server/controllers/rawDataController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import { 3 | CloudWatchLogsClient, 4 | DescribeLogStreamsCommand, 5 | GetLogEventsCommand, 6 | } from '@aws-sdk/client-cloudwatch-logs'; 7 | import { getFunction } from './getFunctionsController'; 8 | import { FormattedLog } from '../types'; 9 | import { getAwsConfig } from '../configs/awsconfig'; 10 | 11 | 12 | interface LogEvent { 13 | message: string; 14 | timestamp: number; 15 | } 16 | 17 | // formats report message string into useable key-value pairs 18 | const formatLogs = ( 19 | logs: { log: LogEvent; functionName: string }[] 20 | ): FormattedLog[] => { 21 | return logs.map(({ log, functionName }) => { 22 | const dateObject = new Date(log.timestamp); 23 | const formattedDate = dateObject 24 | .toLocaleString('en-US', { timeZone: 'UTC' }) 25 | .split(', '); 26 | 27 | const currentFormattedLog: FormattedLog = { 28 | Date: formattedDate[0], 29 | Time: formattedDate[1], 30 | FunctionName: functionName, 31 | }; 32 | 33 | const parts = log.message.split(/\s+/); 34 | 35 | parts.forEach((part, index) => { 36 | if (part === 'Billed') 37 | currentFormattedLog.BilledDuration = parts[index + 2]; 38 | if (part === 'Init') currentFormattedLog.InitDuration = parts[index + 2]; 39 | if (part === 'Max') currentFormattedLog.MaxMemUsed = parts[index + 3]; 40 | }); 41 | 42 | return currentFormattedLog; 43 | }); 44 | }; 45 | 46 | // fetches report data from aws 47 | const fetchAndSaveLogs = async (functionName: string) => { 48 | const formattedFunc = `/aws/lambda/${functionName}`; 49 | const allLogs: { log: LogEvent; functionName: string }[] = []; 50 | 51 | try { 52 | const awsconfig = getAwsConfig(); 53 | const client = new CloudWatchLogsClient(awsconfig); 54 | 55 | const describeResponse = await client.send( 56 | new DescribeLogStreamsCommand({ logGroupName: formattedFunc }) 57 | ); 58 | const streams = describeResponse.logStreams || []; 59 | 60 | // takes the 6 most recent streams 61 | for (const stream of streams.slice(-6)) { 62 | const params = { 63 | logGroupName: formattedFunc, 64 | logStreamName: stream.logStreamName, 65 | startFromHead: true, 66 | }; 67 | 68 | try { 69 | const logsResponse = await client.send(new GetLogEventsCommand(params)); 70 | const events = logsResponse.events || []; 71 | 72 | // loops through all the events, finding messages that start with REPORT 73 | for (const event of events) { 74 | if (event.message && event.message.startsWith('REPORT')) { 75 | allLogs.push({ 76 | log: { 77 | message: event.message, 78 | timestamp: event.timestamp || Date.now(), 79 | }, 80 | functionName: functionName, 81 | }); 82 | } 83 | } 84 | } catch (err) { 85 | console.error( 86 | `Error retrieving log events for stream ${stream.logStreamName}: ${ 87 | (err as Error).message 88 | }` 89 | ); 90 | } 91 | } 92 | } catch (err) { 93 | console.error( 94 | `Error retrieving log streams for log group ${functionName}: ${ 95 | (err as Error).message 96 | }` 97 | ); 98 | } 99 | return formatLogs(allLogs); 100 | }; 101 | 102 | // processes the logs retrieved from the above functions and passes the data to the frontend 103 | const lambdaController = { 104 | async processLogs(req: Request, res: Response, next: NextFunction) { 105 | try { 106 | // retrieves the array of function names 107 | const functionNames: string[] = await getFunction(); 108 | 109 | if (functionNames.length === 0) { 110 | return res.status(404).json({ error: 'No Lambda functions found' }); 111 | } 112 | 113 | // helper function that maps and fetches the log data to a schema 114 | const helper = async (functionNames: string[]) => { 115 | const fetchPromises = functionNames.map(async (functionName) => { 116 | return { 117 | functionName, 118 | logs: await fetchAndSaveLogs(functionName), 119 | }; 120 | }); 121 | 122 | // resolves all the promises 123 | const dataArr = await Promise.all(fetchPromises); 124 | 125 | return dataArr; 126 | }; 127 | 128 | // invokes the helper function and stores the data in a new variable 129 | const dataArr = await helper(functionNames); 130 | 131 | res.locals.allData = dataArr; 132 | 133 | return next(); 134 | } catch (err) { 135 | next({ 136 | log: 'Error in lambdaController.processLogs', 137 | status: 500, 138 | message: { err: 'Error occurred when finding Lambda log streams' }, 139 | }); 140 | } 141 | }, 142 | }; 143 | 144 | export default lambdaController; 145 | -------------------------------------------------------------------------------- /client/src/App.scss: -------------------------------------------------------------------------------- 1 | // element colors 2 | $element-h: #62accc; 3 | $element-s: #447a90; 4 | 5 | // light mode colors 6 | $light-cont-l: #ffffff; 7 | $light-cont-m: #f3f3f3; 8 | $light-cont-s: #e1e1e1; 9 | $light-text-prim: #161616; 10 | $light-text-sec: #646464; 11 | 12 | // dark mode colors 13 | $dark-cont-l: #161616; 14 | $dark-cont-m: #2a2a2a; 15 | $dark-cont-s: #363636; 16 | $dark-text-prim: #ffffff; 17 | $dark-text-sec: #a2a2a2; 18 | 19 | // misc 20 | $font-stack: Helvetica, Arial, sans-serif; 21 | $error: #d25d23; 22 | $reg: 300; 23 | $medium: 400; 24 | $bold: 500; 25 | 26 | $mui-boxshadow-2: 0px 4px 6px rgba(0, 0, 0, 0.1), 27 | 0px 1px 3px rgba(0, 0, 0, 0.08); 28 | 29 | body { 30 | background-color: $light-cont-l; 31 | } 32 | 33 | body.darkmode body { 34 | background-color: $dark-cont-l; 35 | } 36 | 37 | #root { 38 | background-color: $light-cont-l; 39 | } 40 | body.darkmode #root { 41 | background-color: $dark-cont-l; 42 | } 43 | 44 | .logo { 45 | height: 2.7em; 46 | // filter: brightness(0) invert(1); 47 | transition: filter 300ms; 48 | } 49 | 50 | body.darkmode .logo { 51 | filter: brightness(0) invert(1); 52 | transition: filter 300ms; 53 | } 54 | .logo-container { 55 | display: flex; 56 | align-items: center; 57 | } 58 | // navbar container 59 | .navbar { 60 | display: flex; 61 | align-items: center; 62 | justify-content: space-between; 63 | border-radius: 10px; 64 | background-color: $light-cont-m; 65 | position: relative; 66 | font-family: $font-stack; 67 | box-shadow: $mui-boxshadow-2; 68 | } 69 | 70 | .navbar-right { 71 | display: flex; 72 | align-items: center; 73 | gap: 20px; 74 | } 75 | // navbar links 76 | #navbarLinks { 77 | list-style-type: none; 78 | margin: 0; 79 | padding: 0; 80 | display: flex; 81 | gap: 20px; 82 | 83 | li { 84 | margin: 0; 85 | } 86 | 87 | // navbar links 88 | a { 89 | color: $light-text-sec; 90 | text-decoration: none; 91 | padding: 10px 15px; 92 | border-radius: 5px; 93 | transition: background-color 0.3s, color 0.3s; 94 | font-weight: $reg; 95 | font-size: 0.9em; 96 | 97 | &:hover, 98 | &:focus { 99 | background-color: $light-cont-s; 100 | color: $light-text-prim; 101 | text-decoration: none; 102 | } 103 | 104 | &.active { 105 | background-color: $light-cont-s; 106 | color: $light-text-prim; 107 | text-decoration: none; 108 | } 109 | } 110 | } 111 | 112 | body.darkmode #navbarLinks { 113 | list-style-type: none; 114 | margin: 0; 115 | padding: 0; 116 | display: flex; 117 | gap: 20px; 118 | 119 | li { 120 | margin: 0; 121 | } 122 | 123 | // navbar links 124 | a { 125 | color: $dark-text-sec; 126 | text-decoration: none; 127 | padding: 10px 15px; 128 | border-radius: 5px; 129 | transition: background-color 0.3s, color 0.3s; 130 | font-weight: $reg; 131 | font-size: 0.9em; 132 | 133 | &:hover, 134 | &:focus { 135 | background-color: $dark-cont-s; 136 | color: $dark-text-prim; 137 | text-decoration: none; 138 | } 139 | 140 | &.active { 141 | background-color: $dark-cont-s; 142 | color: $dark-text-prim; 143 | text-decoration: none; 144 | } 145 | } 146 | } 147 | 148 | // theme switch button 149 | #theme-switch { 150 | height: 30px; 151 | width: 30px; 152 | padding: 7px; 153 | border-radius: 50%; 154 | background-color: var(--base-variant); 155 | // background-color: $light-text-sec; 156 | display: flex; 157 | justify-content: center; 158 | align-items: center; 159 | // box-shadow: $mui-boxshadow-2; 160 | cursor: pointer; 161 | svg:hover, 162 | svg:focus { 163 | fill: darken($light-text-sec, 10%); 164 | } 165 | } 166 | 167 | // dark mode styles 168 | .darkmode { 169 | .navbar { 170 | background-color: #222; 171 | } 172 | 173 | #navbarLinks a { 174 | color: #fff; 175 | } 176 | 177 | .logo-container img { 178 | // filter: brightness(0.8); 179 | transition: filter 0.3s ease; 180 | } 181 | } 182 | 183 | @media (max-width: 768px) { 184 | .logo-container { 185 | width: 15%; 186 | max-width: 80px; 187 | } 188 | } 189 | 190 | @media (max-width: 480px) { 191 | .logo-container { 192 | width: 20%; 193 | max-width: 60px; 194 | } 195 | } 196 | 197 | // #root { 198 | // margin-top: 0; 199 | // background-color: $light-cont-l; 200 | // } 201 | 202 | // // light mode settings 203 | // .navbar { 204 | // background: $light-cont-m; 205 | // display: flex; 206 | // justify-content: center; 207 | // align-items: center; 208 | // font-size: 20px; 209 | // // z-index: 1000; 210 | // color: rgb(122, 38, 38); 211 | // border-radius: 15px; 212 | 213 | // ul { 214 | // text-align: center; 215 | // margin: 0; 216 | // padding: 15px; 217 | // list-style: none; 218 | // } 219 | 220 | // .navbarLinks { 221 | // color: red; 222 | // } 223 | 224 | // li { 225 | // display: inline-block; 226 | // font-family: $font-stack; 227 | // color: black; 228 | // a { 229 | // color: black; 230 | // text-decoration: none; 231 | // padding: 14px; 232 | // } 233 | // } 234 | // } 235 | 236 | .config-error { 237 | text-align: left; 238 | color: red; 239 | margin-left: 0; 240 | padding-left: 10px; 241 | font-size: 12px; 242 | position: relative; 243 | } 244 | 245 | .config-error::before { 246 | content: '⚠ '; 247 | display: inline-block; 248 | color: red; 249 | margin-right: 5px; 250 | } 251 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs" /* Specify what module code is generated. */, 29 | "rootDir": "./" /* Specify the root folder within your source files. */, 30 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 39 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 40 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 41 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 42 | // "resolveJsonModule": true, /* Enable importing .json files. */ 43 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 44 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 45 | 46 | /* JavaScript Support */ 47 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 48 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 49 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 50 | 51 | /* Emit */ 52 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 53 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 54 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 55 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 56 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 57 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 58 | "outDir": "./dist" /* Specify an output folder for all emitted files. */, 59 | // "removeComments": true, /* Disable emitting comments. */ 60 | // "noEmit": true, /* Disable emitting files from a compilation. */ 61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 62 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 63 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 64 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 65 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 66 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 67 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 68 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 69 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 70 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 71 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 72 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 73 | 74 | /* Interop Constraints */ 75 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 76 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 77 | // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ 78 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 79 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 80 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 81 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 82 | 83 | /* Type Checking */ 84 | "strict": true /* Enable all strict type-checking options. */, 85 | "noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied 'any' type. */, 86 | "strictNullChecks": true /* When type checking, take into account 'null' and 'undefined'. */, 87 | "strictFunctionTypes": true /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */, 88 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 89 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 90 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 91 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 92 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 93 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 94 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 95 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 96 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 97 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 98 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 99 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 100 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 101 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 102 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 103 | 104 | /* Completeness */ 105 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 106 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 107 | }, 108 | "include": [ 109 | "**/*.ts", 110 | "types.ts", 111 | ".env" 112 | ], 113 | "exclude": [ 114 | "node_modules", 115 | "dist" 116 | ], 117 | "ts-node": { 118 | "files": true 119 | }, 120 | } 121 | --------------------------------------------------------------------------------