├── 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 |
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 |
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 | {/*
*/}
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 |
16 | );
17 |
18 | const DarkModeIcon = () => (
19 |
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 &&
}
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 |
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 |
--------------------------------------------------------------------------------