[as sendEmail]"},{"path":"/var/task/src/handler.js","line":63,"label":"Runtime.module.exports.sendNotification [as handler]"},{"path":"/var/runtime/Runtime.js","line":74,"label":"Runtime.handleOnceNonStreaming"}]}]}}]}]},"Id":"6d63ead43f720bac"}}],"origin":"AWS::Lambda","http":{"response":{"status":200}},"fullData":{"Document":{"id":"3825ef9b9ee73c89","name":"todo-list-app-dev-sendNotification","start_time":1681935069.023,"trace_id":"1-64404adc-70a67b4f04679f1750f09a91","end_time":1681935070.117,"parent_id":"6087903191f4a08e","http":{"response":{"status":200}},"aws":{"request_id":"f3f453ae-9e9d-4f26-afe3-e66050dbebd9"},"origin":"AWS::Lambda","resource_arn":"arn:aws:lambda:us-east-1:263792328682:function:todo-list-app-dev-sendNotification"},"Id":"3825ef9b9ee73c89"}},{"id":"053972ae25848977","name":"DynamoDB","parent_id":"5031472a748a4a21","time_taken":29.000043869018555,"children":[],"origin":"AWS::DynamoDB::Table","http":{"response":{"status":200}},"fullData":{"Document":{"id":"053972ae25848977","name":"DynamoDB","start_time":1681935068.974,"trace_id":"1-64404adc-70a67b4f04679f1750f09a91","end_time":1681935069.003,"parent_id":"5031472a748a4a21","inferred":true,"http":{"response":{"status":200}},"aws":{"retries":0,"region":"us-east-1","operation":"PutItem","request_id":"FIMJQ5R22UTD215OR2HTMHOROVVV4KQNSO5AEMVJF66Q9ASUAAJG","table_name":"todo-list-tasks","resource_names":["todo-list-tasks"]},"origin":"AWS::DynamoDB::Table"},"Id":"053972ae25848977"}}],"origin":"AWS::Lambda::Function","fullData":{"Document":{"id":"7dae52540909690d","name":"todo-list-app-dev-postTask","start_time":1681935068.9706764,"trace_id":"1-64404adc-70a67b4f04679f1750f09a91","end_time":1681935070.121799,"parent_id":"33e141e0065cb41b","aws":{"account_id":"263792328682","function_arn":"arn:aws:lambda:us-east-1:263792328682:function:todo-list-app-dev-postTask","resource_names":["todo-list-app-dev-postTask"]},"origin":"AWS::Lambda::Function","subsegments":[{"id":"c43d5f8cd2ad1351","name":"Overhead","start_time":1681935070.1212032,"end_time":1681935070.121594,"aws":{"function_arn":"arn:aws:lambda:us-east-1:263792328682:function:todo-list-app-dev-postTask"}},{"id":"3d95ce606b1fe75b","name":"Invocation","start_time":1681935068.970756,"end_time":1681935070.1211765,"aws":{"function_arn":"arn:aws:lambda:us-east-1:263792328682:function:todo-list-app-dev-postTask"},"subsegments":[{"id":"6087903191f4a08e","name":"Lambda","start_time":1681935069.005,"end_time":1681935070.12,"http":{"response":{"status":200}},"aws":{"retries":0,"status_code":200,"function_name":"todo-list-app-dev-sendNotification","region":"us-east-1","operation":"Invoke","request_id":"f3f453ae-9e9d-4f26-afe3-e66050dbebd9","resource_names":["todo-list-app-dev-sendNotification"]},"namespace":"aws"},{"id":"5031472a748a4a21","name":"DynamoDB","start_time":1681935068.974,"end_time":1681935069.003,"http":{"response":{"status":200}},"aws":{"retries":0,"region":"us-east-1","operation":"PutItem","request_id":"FIMJQ5R22UTD215OR2HTMHOROVVV4KQNSO5AEMVJF66Q9ASUAAJG","table_name":"todo-list-tasks","resource_names":["todo-list-tasks"]},"namespace":"aws"}]}]},"Id":"7dae52540909690d"}}],"origin":"AWS::Lambda","http":{"response":{"status":200}},"fullData":{"Document":{"id":"33e141e0065cb41b","name":"todo-list-app-dev-postTask","start_time":1681935068.964,"trace_id":"1-64404adc-70a67b4f04679f1750f09a91","end_time":1681935070.122,"parent_id":"42979cb403e5bc2b","http":{"response":{"status":200}},"aws":{"request_id":"b3b0d9e3-8c4c-436a-aaaa-1a45823094eb"},"origin":"AWS::Lambda","resource_arn":"arn:aws:lambda:us-east-1:263792328682:function:todo-list-app-dev-postTask"},"Id":"33e141e0065cb41b"}}],"origin":"AWS::ApiGateway::Stage","http":{"request":{"url":"https://cm8rpw34l8.execute-api.us-east-1.amazonaws.com/dev/add-task","method":"POST","user_agent":"PostmanRuntime/7.31.3","client_ip":"172.56.161.54","x_forwarded_for":true},"response":{"status":201,"content_length":0}},"fullData":{"Document":{"id":"5f1c861d6f80b651","name":"dev-todo-list-app/dev","start_time":1681935068.952,"trace_id":"1-64404adc-70a67b4f04679f1750f09a91","end_time":1681935070.124,"http":{"request":{"url":"https://cm8rpw34l8.execute-api.us-east-1.amazonaws.com/dev/add-task","method":"POST","user_agent":"PostmanRuntime/7.31.3","client_ip":"172.56.161.54","x_forwarded_for":true},"response":{"status":201,"content_length":0}},"aws":{"api_gateway":{"account_id":"263792328682","rest_api_id":"cm8rpw34l8","stage":"dev","request_id":"beb4cd2d-9a3a-41f7-b70c-a230e95e7bf3"}},"annotations":{"aws:api_id":"cm8rpw34l8","aws:api_stage":"dev"},"metadata":{"default":{"extended_request_id":"DpCikGdJIAMF9SQ=","request_id":"beb4cd2d-9a3a-41f7-b70c-a230e95e7bf3"}},"origin":"AWS::ApiGateway::Stage","resource_arn":"arn:aws:apigateway:us-east-1::/restapis/cm8rpw34l8/stages/dev","subsegments":[{"id":"42979cb403e5bc2b","name":"Lambda","start_time":1681935068.954,"end_time":1681935070.123,"http":{"request":{"url":"https://lambda.us-east-1.amazonaws.com/2015-03-31/functions/arn:aws:lambda:us-east-1:263792328682:function:todo-list-app-dev-postTask/invocations","method":"POST"},"response":{"status":201,"content_length":2}},"aws":{"function_name":"todo-list-app-dev-postTask","region":"us-east-1","operation":"Invoke","resource_names":["todo-list-app-dev-postTask"]},"namespace":"aws"}]},"Id":"5f1c861d6f80b651"},"cold_start":true,"logs":[{"eventId":"37508405413572999978249291127328083047567788380815228928","ingestionTime":1681935077994,"logStreamName":"2023/04/19/[$LATEST]3adf2feaed664eccac9b695a47de0c9d","message":"START RequestId: b3b0d9e3-8c4c-436a-aaaa-1a45823094eb Version: $LATEST\n","timestamp":1681935068970},{"eventId":"37508405413595300723447821750469618765840436742321209345","ingestionTime":1681935077994,"logStreamName":"2023/04/19/[$LATEST]3adf2feaed664eccac9b695a47de0c9d","message":"2023-04-19T20:11:08.971Z\tb3b0d9e3-8c4c-436a-aaaa-1a45823094eb\tINFO\tpost task controller\n","timestamp":1681935068971},{"eventId":"37508405414331225314999332314140297468837832672018563074","ingestionTime":1681935077994,"logStreamName":"2023/04/19/[$LATEST]3adf2feaed664eccac9b695a47de0c9d","message":"2023-04-19T20:11:09.004Z\tb3b0d9e3-8c4c-436a-aaaa-1a45823094eb\tINFO\tpost task controller\n","timestamp":1681935069004},{"eventId":"37508405439241157701758038363235694779386052474198687747","ingestionTime":1681935077994,"logStreamName":"2023/04/19/[$LATEST]3adf2feaed664eccac9b695a47de0c9d","message":"END RequestId: b3b0d9e3-8c4c-436a-aaaa-1a45823094eb\n","timestamp":1681935070121},{"eventId":"37508405439241157701758038363235694779386052474198687748","ingestionTime":1681935077994,"logStreamName":"2023/04/19/[$LATEST]3adf2feaed664eccac9b695a47de0c9d","message":"REPORT RequestId: b3b0d9e3-8c4c-436a-aaaa-1a45823094eb\tDuration: 1151.25 ms\tBilled Duration: 1152 ms\tMemory Size: 1024 MB\tMax Memory Used: 102 MB\t\nXRAY TraceId: 1-64404adc-70a67b4f04679f1750f09a91\tSegmentId: 7dae52540909690d\tSampled: true\t\n","timestamp":1681935070121}]}]
--------------------------------------------------------------------------------
/client/src/assets/sesIcon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/assets/settings-svgrepo-com.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/client/src/assets/simpleNotification.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/assets/team-svgrepo-com.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/client/src/components/AppTree.jsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/LambdaPulse/f6ef43b6567ed2d02946abd5edc3c129628930b2/client/src/components/AppTree.jsx
--------------------------------------------------------------------------------
/client/src/components/DebugTraceDisplay.jsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import DataTable, { createTheme } from 'react-data-table-component'
3 | import TraceFilters from './TraceFilters'
4 |
5 | const columns = [
6 | {
7 | name: 'name',
8 | selector: row => row["name"],
9 | width:"250px",
10 | sortable: true,
11 | },
12 | {
13 | name: 'start time',
14 | selector: row => row["start_time"],
15 | width:"200px",
16 | sortable: true,
17 | },
18 | {
19 | name: 'duration',
20 | selector: row => row["duration"],
21 | width:"100px",
22 | sortable: true,
23 | },
24 | {
25 | name: 'error',
26 | selector: row => row["error"],
27 | width:"100px",
28 | sortable: true,
29 | },
30 | {
31 | name: 'cause',
32 | selector: row => row["cause"],
33 | width:"400px",
34 | wrap:true
35 | },
36 | {
37 | name: 'status',
38 | selector: row => row["status"],
39 | width:"100px",
40 | sortable: true,
41 | },
42 | {
43 | name: 'origin',
44 | selector: row => row["origin"],
45 | width:"200px",
46 | sortable: true,
47 | },
48 | {
49 | name: 'subsegments',
50 | selector: row => row["subsegments"],
51 | width:"200px",
52 | sortable: true,
53 | },
54 | {
55 | name: 'method',
56 | selector: row => row["method"],
57 | width:"200px",
58 | sortable: true,
59 | },
60 | {
61 | name: 'fulldata',
62 | selector: row => row["fullData"],
63 | width:"200px",
64 | sortable: true,
65 | },
66 |
67 | //{
68 | // name: 'subsegments',
69 | // selector: row => row["subsegments"],
70 | // width:"200px",
71 | //},
72 | ]
73 |
74 | createTheme('dark', {
75 | text: {
76 | primary: '#dddddd',
77 | secondary: '#2aa198',
78 | },
79 | background: {
80 | default: '#222222',
81 | },
82 | context: {
83 | background: '#cb4b16',
84 | text: '#FFFFFF',
85 | },
86 | divider: {
87 | default: '#073642',
88 | },
89 | action: {
90 | button: 'rgba(0,0,0,.54)',
91 | hover: 'rgba(0,0,0,.08)',
92 | disabled: 'rgba(0,0,0,.12)',
93 | },
94 | }, 'dark');
95 |
96 | const getStartLocale = (startSec) => {
97 | let start = new Date(0);
98 | start.setUTCSeconds(startSec);
99 | return start.toLocaleString()
100 | }
101 |
102 |
103 | const flattenTrace = (trace) => {
104 | const result = [];
105 | if (!trace) {
106 | console.log('Can\'t flatten trace; it doesn\'t exist!');
107 | }
108 |
109 | const process = (node) => {
110 | if (!node) {
111 | console.log('Error! No node...')
112 | return;
113 | }
114 | if (!node.fullData) {
115 | console.log('Error! Not getting full data from node...');
116 | return;
117 | }
118 |
119 | const nr = {};
120 | const n = node.fullData.Document;
121 |
122 | if (n['start_time']) nr.start_time = getStartLocale(n['start_time']);
123 | if (n['start_time'] && n['end_time']) nr.duration = Math.floor( (n['end_time'] - n['start_time']) * 1000)/1000;
124 | nr.name = n['name'];
125 | if (n.http && n.http.response) nr.status = n.http.response.status;
126 | if (n.cause) {
127 | nr.cause = n.cause.message;
128 | for (const e in n.cause.exceptions) {
129 | nr.cause += n.cause.exceptions[e].message;
130 | }
131 | }
132 | if (n.error) nr.error = 'yes';
133 | if (n.origin) nr.origin = n.origin;
134 | if (n.subsegments) {
135 | let c = 0;
136 | for (let i = 0; i < n.subsegments.length; i++) {
137 | c++;
138 | }
139 | nr.subsegments = c;
140 | }
141 |
142 | for (const c in node.children) {
143 | process(node.children[c]);
144 | }
145 |
146 | result.push(nr);
147 | }
148 |
149 | process(trace);
150 |
151 | return result;
152 | }
153 |
154 | const DebugTraceDisplay = (props) => {
155 |
156 | return (
157 |
158 |
Segment Detail:
159 |
165 |
166 | )
167 | };
168 |
169 | export default DebugTraceDisplay;
170 |
--------------------------------------------------------------------------------
/client/src/components/EventGraph.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import NodeTree from './NodeTree';
3 | import NodeDetail from './NodeDetail';
4 | import LogPanel from './LogPanel';
5 | import NavBar from './NavBar';
6 | import TraceList from './TraceList';
7 | import './event-graph.css';
8 |
9 | const EventGraph = (props) => {
10 | const [nodeDetailState,setNodeDetailState] = useState({left: 150, top:150, display: 'none', curNode: null});
11 |
12 | return (
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | )
23 | };
24 |
25 | export default EventGraph;
26 |
--------------------------------------------------------------------------------
/client/src/components/HomeDisplay.css:
--------------------------------------------------------------------------------
1 | .metrics__home__container {
2 | position: relative;
3 | display: grid;
4 | }
5 | .trace-list-container {
6 | position: relative;
7 | display: flex;
8 | }
9 |
10 | .trace-list-traces {
11 | display: flex;
12 | flex-direction: column;
13 | width: 15%
14 | }
15 |
16 | .debug-trace-display {
17 | position: relative;
18 | overflow-inline: scroll;
19 | max-width: 85%;
20 | }
21 |
22 | .loading-spinner {
23 | position: absolute;
24 | top: 50%;
25 | left: 50%;
26 | transform: translate(-50%, -50%);
27 | z-index: 100;
28 | }
29 |
30 | .datepicker__override {
31 | color: #ffffff;
32 | }
33 |
34 | .datepicker__popper__override div div div {
35 | background-color: #232323;
36 | color: #ffffff
37 | }
38 |
39 | .filter__label {
40 | font-size: x-small;
41 | float: left;
42 | }
43 |
44 | .filter__container {
45 | display: inline-block;
46 | margin-left: 10px;
47 | margin-right: 10px;
48 | }
--------------------------------------------------------------------------------
/client/src/components/HomeDisplay.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { useNavigate } from 'react-router-dom';
3 | import SimpleDataDisplay from './SimpleDataDisplay';
4 | import { PieChart } from 'react-minimal-pie-chart';
5 | import spinner from '../assets/pulse-1.1s-200px.svg';
6 | import './HomeDisplay.css';
7 |
8 | const getStartLocale = (startSec) => {
9 | let start = new Date(0);
10 | start.setUTCSeconds(startSec);
11 | return start.toLocaleString();
12 | };
13 |
14 | const flattenTrace = (trace) => {
15 | const result = [];
16 | if (!trace) return;
17 |
18 | const process = (node) => {
19 | const nr = {};
20 | const n = node.fullData.Document;
21 |
22 | if (n['start_time']) nr.start_time = getStartLocale(n['start_time']);
23 | if (n['start_time'] && n['end_time'])
24 | nr.duration = Math.floor((n['end_time'] - n['start_time']) * 1000) / 1000;
25 | nr.name = n['name'];
26 | if (n.http && n.http.response) nr.status = n.http.response.status;
27 | if (n.cause) {
28 | nr.cause = n.cause.message;
29 | for (const e in n.cause.exceptions) {
30 | nr.cause += n.cause.exceptions[e].message;
31 | }
32 | }
33 | if (n.error) nr.error = 'yes';
34 | if (n.origin) nr.origin = n.origin;
35 | if (n.subsegments) {
36 | let c = 0;
37 | for (let i = 0; i < n.subsegments.length; i++) {
38 | c++;
39 | }
40 | nr.subsegments = c;
41 | }
42 |
43 | for (const c in node.children) {
44 | process(node.children[c]);
45 | }
46 |
47 | result.push(nr);
48 | };
49 |
50 | process(trace);
51 |
52 | return result;
53 | };
54 |
55 | const traceHasErrors = (trace) => {
56 | let segmentQueue = [];
57 | segmentQueue.push(trace);
58 | while (segmentQueue.length) {
59 | let s = segmentQueue.pop();
60 | s.children.forEach((segment) => segmentQueue.push(segment));
61 | if (s.fullData && s.fullData.Document.error) return true;
62 | }
63 | return false;
64 | }
65 |
66 | const getFromRight = (s) => {
67 | let result = '';
68 | for (let i = s.length-1; i >= 0; i--) {
69 | if (s[i] == '/') return result;
70 | result = s[i] + result;
71 | }
72 | return result;
73 | }
74 |
75 | const uniqueFunctions = (traces) => {
76 | const uniques = {};
77 | for (const t in traces) {
78 | if (traces[t].fullData && traces[t].fullData.Document.http.request) {
79 | const u = getFromRight(traces[t].fullData.Document.http.request.url);
80 | if (!uniques[u]) uniques[u] = 0;
81 | uniques[u]++;
82 | }
83 | }
84 | return uniques;
85 | }
86 |
87 | const tallyOrigins = (flattenedTrace) => {
88 | const result = {};
89 |
90 | for (const f in flattenedTrace) {
91 | let origin = flattenedTrace[f].origin;
92 | if (result[origin]) result[origin]++;
93 | else result[origin] = 1;
94 | }
95 |
96 | return result;
97 | };
98 |
99 | const tallyToCircle = (tally) => {
100 | const result = [];
101 | const colors = ['#E38627', '#C13C37', '#6A2135'];
102 | let curColor = 0;
103 | for (const k in tally) {
104 | const r = {};
105 | r.title = k;
106 | r.value = tally[k];
107 | r.color = colors[curColor];
108 | curColor++;
109 | if (curColor >= colors.length) curColor = 0;
110 | result.push(r);
111 | }
112 | return result;
113 | };
114 |
115 | const uniqueFunctionsToCircle = (uniqueFunctions) => {
116 | const result = [];
117 | const colors = ['#E38627', '#C13C37', '#6A2135'];
118 | let curColor = 0;
119 | for (const k in uniqueFunctions) {
120 | const r = {};
121 | r.title = k;
122 | r.value = uniqueFunctions[k];
123 | r.color = colors[curColor];
124 | curColor++;
125 | if (curColor >= colors.length) curColor = 0;
126 | result.push(r);
127 | }
128 |
129 | return result;
130 | }
131 |
132 | const HomeDisplay = (props) => {
133 |
134 | const invocationCount = props.traces ? props.traces.length : 0;
135 | let successCount = 0;
136 | let errorCount = 0;
137 | for (const t in props.traces) {
138 | const n = props.traces[t];
139 | if (traceHasErrors(n)) errorCount++;
140 | else successCount++;
141 | }
142 |
143 | let sumDuration = 0;
144 | let countDuration = 0;
145 | for (const t in props.traces) {
146 | const n = props.traces[t];
147 | if (n.averageTime) {
148 | sumDuration += n.averageTime;
149 | countDuration++;
150 | }
151 | }
152 | const avgDuration = (sumDuration > 0 ? Math.floor(sumDuration / countDuration * 1)/1 : 0) + ' ms';
153 |
154 | const origins = tallyToCircle(tallyOrigins(flattenTrace(props.traces[props.currentTrace])));
155 |
156 | const uniqueFuncs = uniqueFunctionsToCircle(uniqueFunctions(props.traces));
157 |
158 |
159 | return (
160 |
161 | {props.loading && (

)}
162 |
163 |
164 |
165 |
166 |
167 |
{return props.dataEntry.title} }
178 | labelStyle={{
179 | fill: 'white',
180 | fontSize: '4px'
181 | }}
182 | />
183 |
184 |
185 |
{return props.dataEntry.title} }
196 | labelStyle={{
197 | fill: 'white',
198 | fontSize: '4px'
199 | }}
200 | />
201 |
202 |
203 |
204 | )
205 | }
206 |
207 | export default HomeDisplay;
208 |
--------------------------------------------------------------------------------
/client/src/components/LogPanel.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import DataTable, { createTheme } from 'react-data-table-component';
3 | import sampleLog from '../assets/sampleLog';
4 |
5 | const columns = [
6 | {
7 | name: 'timestamp',
8 | selector: (row) => row['timestamp'],
9 | width: '180px',
10 | format: d => {
11 | return (new Date(d.timestamp).toLocaleString() )
12 | }
13 | },
14 | {
15 | name: 'message',
16 | selector: (row) => row['message'],
17 | width: '800px',
18 | wrap: true
19 | },
20 | {
21 | name: 'logStreamName',
22 | selector: (row) => row['logStreamName'],
23 | width: '350px',
24 | },
25 | {
26 | name: 'eventId',
27 | selector: row => row["eventId"],
28 | width:"350px"
29 | },
30 | ];
31 |
32 | createTheme('dark', {
33 | text: {
34 | primary: '#dddddd',
35 | secondary: '#2aa198',
36 | },
37 | background: {
38 | default: '#222222',
39 | },
40 | context: {
41 | background: '#cb4b16',
42 | text: '#FFFFFF',
43 | },
44 | divider: {
45 | default: '#073642',
46 | },
47 | action: {
48 | button: 'rgba(0,0,0,.54)',
49 | hover: 'rgba(0,0,0,.08)',
50 | disabled: 'rgba(0,0,0,.12)',
51 | },
52 | }, 'dark');
53 |
54 |
55 | const customStyles = {
56 | rows: {
57 | style: {
58 | minHeight: '72px', // override the row height
59 | },
60 | },
61 | headCells: {
62 | style: {
63 | paddingLeft: '8px', // override the cell padding for head cells
64 | paddingRight: '8px',
65 | },
66 | },
67 | cells: {
68 | style: {
69 | paddingLeft: '8px', // override the cell padding for data cells
70 | paddingRight: '8px',
71 | },
72 | },
73 | };
74 |
75 |
76 |
77 | const LogPanel = (props) => {
78 |
79 | return (
80 |
81 |
Log Detail:
82 |
88 |
89 | );
90 | };
91 |
92 | export default LogPanel;
93 |
--------------------------------------------------------------------------------
/client/src/components/Metrics.jsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { PieChart } from 'react-minimal-pie-chart';
3 |
4 | const Metrics = (props) => {
5 | return (
6 |
28 | )
29 | };
30 |
31 | export default Metrics;
32 |
--------------------------------------------------------------------------------
/client/src/components/NavBar.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './nav-bar.css';
3 | import pulse from '../assets/pulse.svg';
4 | import { Link } from 'react-router-dom';
5 |
6 | const NavBar = (props) => {
7 | return (
8 |
9 |
10 |
11 |

12 |
13 |
LambdaPulse
14 |
15 |
27 |
28 | );
29 | };
30 |
31 | export default NavBar;
32 |
--------------------------------------------------------------------------------
/client/src/components/NodeDetail.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import './custom-tree.css';
3 |
4 |
5 |
6 | const NodeDetail = (props) => {
7 |
8 | const attrs = [];
9 |
10 | if (props.nds.curNode) {
11 |
12 | const nodeDetails = [];
13 | const cn = props.nds.curNode;
14 | if (cn.id) nodeDetails.push(id: {cn.id}
);
15 | if (cn.origin) nodeDetails.push(origin: {cn.origin}
);
16 | if (cn.http && cn.http.request) nodeDetails.push(req url: {cn.http.request.url}
);
17 | if (cn.http && cn.http.request) nodeDetails.push(req method: {cn.http.request.method}
);
18 | if (cn.http && cn.http.response) nodeDetails.push(res status: {cn.http.response.status}
);
19 | if (cn.time_taken) nodeDetails.push(time taken ms: {cn.time_taken}
);
20 | if (cn.averageTime) nodeDetails.push(average time ms: {cn.averageTime}
);
21 | if (cn.cold_start) nodeDetails.push(cold start: {cn.cold_start}
);
22 | if (cn.fullData && cn.fullData.Document.error) nodeDetails.push(error: {cn.fullData.Document.error? 'yes' : 'no'}
);
23 | if (cn.fullData && cn.fullData.Document.cause) {
24 | nodeDetails.push(cause: {cn.fullData.Document.cause.message}
);
25 | for (const e in cn.fullData.Document.cause.exceptions) {
26 | nodeDetails.push(exception: {cn.fullData.Document.cause.exceptions[e].message}
);
27 | }
28 | }
29 |
30 |
31 | const subSegments = [];
32 | for (const s in props.nds.curNode.subsegments) {
33 | const curSubSeg = props.nds.curNode.subsegments[s];
34 | let start = new Date(0);
35 | start.setUTCSeconds(curSubSeg.start_time);
36 | start = start.toLocaleString()
37 | let duration = curSubSeg.end_time - curSubSeg.start_time;
38 | subSegments.push(
39 |
subsegment name: {curSubSeg.name}
40 |
start time: {start}
41 |
duration: {duration}
42 |
)
43 | }
44 |
45 | const subsegmentBox = {subSegments}
46 | nodeDetails.push(subsegmentBox);
47 |
48 | const nodeDetailBox = {nodeDetails}
49 | attrs.push(nodeDetailBox);
50 | }
51 |
52 | return (
53 |
54 |
{props.nds.curNode ? props.nds.curNode.name : 'n/a'}
55 |
56 | {attrs}
57 |
58 |
59 | )
60 | };
61 |
62 | export default NodeDetail;
63 |
--------------------------------------------------------------------------------
/client/src/components/NodeTree.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Tree from 'react-d3-tree';
3 | import dynamoDBIcon from '../assets/dynamoDB.svg'
4 | import lambdaFuncIcon from '../assets/lambdaFunc.svg'
5 | import simpleNotificationIcon from '../assets/simpleNotification.svg'
6 | import apiGatewayEndpointIcon from '../assets/apigatewayendpoint.svg'
7 | import sesIcon from '../assets/sesIcon.svg'
8 | import errorIcon from '../assets/error-905.svg'
9 | import errorIcon2 from '../assets/error-svgrepo-com.svg'
10 | import rs from '../assets/react.svg'
11 | import clientIcon from '../assets/client-svgrepo-com.svg'
12 | import './custom-tree.css';
13 |
14 | /*
15 | name = ""
16 | id = ""
17 | origin = ""
18 | fullData -> Document = {}
19 | children = []
20 |
21 | */
22 |
23 | const NodeTree = (props) => {
24 | const getDynamicPathClass = ({ source, target }, orientation) => {
25 | if (!target.children) {
26 | // Target node has no children -> this link leads to a leaf node.
27 | return 'link__to-leaf';
28 | }
29 |
30 | // Style it as a link connecting two branch nodes by default.
31 | return 'link__to-branch';
32 | };
33 |
34 | const getIcon = (nType) => {
35 | if (nType == 'AWS::Lambda') return lambdaFuncIcon;
36 | else if (nType == 'AWS::Lambda::Function') return lambdaFuncIcon;
37 | else if (nType == 'AWS::DynamoDB::Table') return dynamoDBIcon;
38 | else if (nType == 'AWS::ApiGateway::Stage') return apiGatewayEndpointIcon;
39 | else if (nType == 'AWS::SES') return sesIcon;
40 | else if (nType == 'simpleNotification') return simpleNotificationIcon;
41 | else if (nType == 'thrownError') return errorIcon;
42 | else if (nType == 'client') return clientIcon;
43 | else {
44 | console.log('Got unknown origin: ' + nType)
45 | return rs;
46 | }
47 | }
48 |
49 | const getColor = (nType) => {
50 | if (nType == 'AWS::Lambda') return 'orange';
51 | else if (nType == 'AWS::Lambda::Function') return 'red';
52 | else if (nType == 'AWS::DynamoDB::Table') return 'blue';
53 | else if (nType == 'AWS::ApiGateway::Stage') return '#4D27AA';
54 | else if (nType == 'AWS::SES') return 'blue';
55 | else if (nType == 'simpleNotification') return 'pink';
56 | else if (nType == 'thrownError') return 'gray';
57 | else return 'gray';
58 | }
59 |
60 | const handleClick = (e,logs) => {
61 | props.setLData({logs:logs});
62 | }
63 |
64 | const handleMouseEnter = (e,curNode) => {
65 | props.setNds({top: e.clientY+10, left: e.clientX+10, display: 'inline', curNode: curNode});
66 | }
67 |
68 | const handleMouseLeave = (e) => {
69 | props.setNds({top: e.clientY+10, left: e.clientX+10, display: 'none', curNode: null});
70 | }
71 |
72 | const getErrorDim = (e) => {
73 | if (e) return 24;
74 | else return 0;
75 | }
76 |
77 |
78 | const renderRectSvgNode = ({ nodeDatum, toggleNode }) => (
79 |
80 | handleClick(e,nodeDatum.logs)}
83 | />
84 | handleClick(e,nodeDatum.logs)}
87 | />
88 | handleClick(e,nodeDatum.logs)}/>
91 | handleClick(e,nodeDatum.logs)}
93 | onMouseEnter={(e) => handleMouseEnter(e, nodeDatum)}
94 | onMouseLeave={(e) => handleMouseLeave(e)}
95 | />
96 |
97 |
99 |
100 | {nodeDatum.name}
101 |
102 |
103 | );
104 |
105 | const initialTranslate = {x:150, y:100};
106 | const textLayout = {textAnchor: "start", x: 10, y: 50, transform: undefined }
107 | const nodeSize = {x:160+40, y:80};
108 |
109 | let nodeData;
110 | if (!props.nData) {
111 | nodeData = {
112 | children: [],
113 | http: {request: {}, response: {}},
114 | id: "abc123",
115 | name: "no-traces-found",
116 | origin: "thrownError",
117 | parent_id: undefined,
118 | subsegments: [{}]}
119 | }
120 | else nodeData = props.nData;
121 |
122 |
123 | return (
124 | // `` will fill width/height of its container; in this case `#treeWrapper`.
125 |
126 |
145 |
146 |
147 | );
148 | }
149 |
150 | export default NodeTree;
--------------------------------------------------------------------------------
/client/src/components/Settings.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { getApiBaseUrl } from '../apiBaseUrl.js';
3 |
4 | const Settings = () => {
5 | const [arn, setArn] = useState('');
6 | const [currentArn, setCurrentArn] = useState('');
7 | const [success, setSuccess] = useState('');
8 |
9 | useEffect(() => {
10 | console.log('in useffect');
11 | fetch(`${getApiBaseUrl()}/getCurrentArn`, {
12 | method: 'GET',
13 | headers: {
14 | 'Content-type': 'application/json',
15 | },
16 | })
17 | .then((result) => result.json())
18 | .then((data) => {
19 | console.log(data.rows[0].role_arn);
20 | if (data.rows[0].role_arn !== null) {
21 | setCurrentArn(data.rows[0].role_arn);
22 | }
23 | });
24 | // console.log(result);
25 | // setCurrentArn(result);
26 | }, []);
27 |
28 | const addArn = (e) => {
29 | console.log('adding arn');
30 | let userData = { userARN: arn };
31 | fetch(`${getApiBaseUrl()}/setUserARN`, {
32 | method: 'POST',
33 | headers: {
34 | 'Content-type': 'application/json',
35 | },
36 | body: JSON.stringify(userData),
37 | })
38 | .then((result) => {
39 | console.log('this is result', result);
40 | setSuccess('User ARN successfully updated!');
41 | })
42 | .catch((err) => console.log(err));
43 | console.log('finished adding arn');
44 | };
45 | return (
46 |
47 |
Settings
48 |
Current Arn: {currentArn}
49 |
50 | Click{' '}
51 |
52 | here
53 |
54 | , create your stack, navigate to the outputs tab, and paste your ARN key
55 | into the field below!
56 |
57 |
{success}
58 |
74 |
75 | );
76 | };
77 |
78 | export default Settings;
79 |
--------------------------------------------------------------------------------
/client/src/components/SimpleDataDisplay.jsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 |
3 | const SimpleDataDisplay = (props) => {
4 | return (
5 |
6 |
{props.label}:
7 | {props.metric}
8 |
9 | )
10 | };
11 |
12 | export default SimpleDataDisplay;
13 |
--------------------------------------------------------------------------------
/client/src/components/TraceFilters.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react"
2 | import DatePicker from 'react-datepicker';
3 | //import 'react-calendar/dist/Calendar.css';
4 | //import 'react-clock/dist/Clock.css';
5 | import "react-datepicker/dist/react-datepicker.css"
6 | import refreshIcon from '../assets/refresh-svgrepo-com.svg'
7 |
8 | const TraceFilters = (props) => {
9 |
10 |
11 | return (
12 |
13 |
Filters
14 |
15 |
16 | Start Time:
17 |
28 |
29 |
30 | End Time:
31 |
42 |
43 |
44 | Refresh:
45 |
48 |
49 |
50 | )
51 | };
52 |
53 | export default TraceFilters;
54 |
--------------------------------------------------------------------------------
/client/src/components/TraceList.jsx:
--------------------------------------------------------------------------------
1 | import {useEffect,useState} from 'react';
2 | import DebugTraceDisplay from './DebugTraceDisplay'
3 | import spinner from '../assets/pulse-1.1s-200px.svg';
4 | import './HomeDisplay.css';
5 | import TraceSelector from './TraceSelector'
6 | import TraceFilters from './TraceFilters';
7 |
8 | function TraceList (props) {
9 |
10 |
11 | function refreshData() {
12 | //clears Traces table from redis
13 | fetch('/clearTraces', {
14 | method: 'GET',
15 | }).then((result) => {
16 | console.log(result);
17 | });
18 | //changes refresh which fires off useEffect(in dashboard.jsx) to fetch new data
19 | props.setRefresh(!props.refresh);
20 | }
21 | return (
22 |
23 |
24 | {props.loading && (
25 |

26 | )}
27 |
28 |
32 |
33 |
36 |
37 |
38 |
39 | );
40 | }
41 | export default TraceList;
42 |
--------------------------------------------------------------------------------
/client/src/components/TraceSelector.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react"
2 | import errorImage from '../assets/error-svgrepo-com.svg';
3 |
4 |
5 | const getFromRight = (s) => {
6 | let result = '';
7 | for (let i = s.length-1; i >= 0; i--) {
8 | if (s[i] == '/') return result;
9 | result = s[i] + result;
10 | }
11 | return result;
12 | }
13 |
14 | const findErrorsInTrace = (trace) => {
15 | let errors = 0;
16 |
17 | const process = (node) => {
18 | if (node.fullData && node.fullData.Document.error) errors++;
19 | if (node.children) node.children.forEach((n) => {process(n)})
20 | }
21 |
22 | process(trace);
23 | return errors;
24 | }
25 |
26 | const TraceSelector = (props) => {
27 | const traces = [];
28 | for (let n = 0; n < props.traces.length; n++) {
29 | const url = props.traces[n].fullData.Document.http.request.url;
30 | const endpt = getFromRight(url)
31 | const startTime = props.traces[n].fullData.Document.start_time;
32 | const endTime = props.traces[n].fullData.Document.end_time;
33 | let startFilter;
34 | try {startFilter = props.start_value.getTime()}
35 | catch (e) {startFilter = props.start_value}
36 | startFilter /= 1000;
37 | let endFilter;
38 | try {endFilter = props.end_value.getTime()}
39 | catch (e) {endFilter = props.end_value}
40 | endFilter /= 1000;
41 |
42 |
43 | if (startTime >= startFilter && endTime <= endFilter) {
44 | let errors = findErrorsInTrace(props.traces[n]);
45 | traces.push()
65 | }
66 | }
67 |
68 |
69 |
70 | return (
71 |
72 |
TraceList
73 | {traces}
74 |
75 |
76 | )
77 | };
78 |
79 | export default TraceSelector;
80 |
--------------------------------------------------------------------------------
/client/src/components/custom-tree.css:
--------------------------------------------------------------------------------
1 | /* custom-tree.css */
2 |
3 | svg g .rd3t-link {
4 | stroke: #999;
5 | }
6 |
7 | .link__to-branch {
8 | stroke-width: 1;
9 | stroke: #f0f;
10 | border-color: #dddddd;
11 | color: #dddddd;
12 | }
13 |
14 | .link__to-leaf {
15 | stroke-width: 1;
16 | stroke: #f0f;
17 | border-color: #dddddd;
18 | color: #dddddd;
19 | }
20 |
21 | .node__detail {
22 | min-width: 200px;
23 | max-width: 300px;
24 | min-height: 200px;
25 | max-height: 400px;
26 | color: #dddddd;
27 | background-color: #191919;
28 | border-radius: 10px;
29 | border-width: 1px;
30 | border-style: dotted;
31 | border-color: #777777;
32 | top: 40px;
33 | left: 40px;
34 | position: absolute;
35 | display: inline;
36 | font-size: small;
37 | }
38 |
39 | .log__list {
40 | background-color: #333333;
41 | margin: 20px;
42 | }
43 |
44 | .nd__attribute {
45 |
46 | }
47 |
48 | .nd__subsegment__container {
49 | font-size: x-small;
50 | background-color: #242424;
51 | padding: 2px;
52 | margin: 2px;
53 | }
54 |
55 | .nd__subsegment {
56 |
57 | }
--------------------------------------------------------------------------------
/client/src/components/event-graph.css:
--------------------------------------------------------------------------------
1 | .EventGraph {
2 | width: 100%;
3 | display:flex;
4 | justify-content: space-between;
5 | }
6 |
7 | .EventPanelContainer {
8 | width:50%;
9 | display: flex;
10 | background-color: #252525;
11 | }
12 |
13 | .LogPanel {
14 | background-color: #191919;
15 | color: #dddddd;
16 | width: 50%;
17 | position:relative;
18 | right:0px;
19 | scroll-behavior:auto;
20 | }
21 |
22 | .metrics__info__display {
23 | background-color: #191919;
24 | height: 160px;
25 | width: 300px;
26 | padding: 40px;
27 | margin: 10px;
28 | }
29 |
30 | .metrics__info__display h2 {
31 | color: #ff3300;
32 | }
33 |
34 | .metrics__visual__display {
35 | background-color: #191919;
36 | height: 340px;
37 | width: 300px;
38 | padding: 4px;
39 | margin: 10px;
40 | }
41 |
42 | .metrics__home__container {
43 | display: grid;
44 | grid-template-columns: 1fr 1fr 1fr
45 | }
46 |
47 |
48 |
--------------------------------------------------------------------------------
/client/src/components/nav-bar.css:
--------------------------------------------------------------------------------
1 | .NavBar{
2 | display:flex;
3 | flex-direction: row;
4 | justify-content: space-between;
5 | border-style: inset;
6 | border-color:rgb(93, 93, 93);
7 | border-width: 3px;
8 | }
9 | #logo{
10 | display:flex;
11 | padding-left:10px;
12 | }
13 | #links{
14 | position:relative;
15 | top: 35px;
16 | display:flex;
17 | justify-content: space-around;
18 | width:200px;
19 | color:rgb(255, 51, 0);
20 | }
--------------------------------------------------------------------------------
/client/src/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
3 | line-height: 1.5;
4 | font-weight: 400;
5 |
6 | color-scheme: light dark;
7 | color: rgba(255, 255, 255, 0.87);
8 | background-color: #242424;
9 |
10 | font-synthesis: none;
11 | text-rendering: optimizeLegibility;
12 | -webkit-font-smoothing: antialiased;
13 | -moz-osx-font-smoothing: grayscale;
14 | -webkit-text-size-adjust: 100%;
15 | }
16 |
17 | a {
18 | font-weight: 500;
19 | color: #ff3300;
20 | text-decoration: inherit;
21 | }
22 | a:hover {
23 | color: #ff3300;
24 | }
25 |
26 | body {
27 | margin: 0;
28 | place-items: center;
29 | min-width: 320px;
30 | min-height: 100vh;
31 | background-color: #111111;
32 | }
33 |
34 | h1 {
35 | font-size: 3.2em;
36 | line-height: 1.1;
37 | }
38 |
39 | button {
40 | border-radius: 8px;
41 | border: 1px solid transparent;
42 | padding: 0.6em 1.2em;
43 | font-size: 1em;
44 | font-weight: 500;
45 | font-family: inherit;
46 | background-color: #1a1a1a;
47 | cursor: pointer;
48 | transition: border-color 0.25s;
49 | }
50 | button:hover {
51 | border-color: #ff3300;
52 | }
53 | button:focus,
54 | button:focus-visible {
55 | }
56 |
57 | @media (prefers-color-scheme: light) {
58 | :root {
59 | color: #dddddd;
60 | background-color: #ffffff;
61 | }
62 | a:hover {
63 | color: #ff3300;
64 | }
65 | button {
66 | background-color: #232323;
67 | color: #ff3300;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/client/src/main.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import { BrowserRouter } from 'react-router-dom';
4 | import App from './App'
5 | import './index.css'
6 |
7 | ReactDOM.createRoot(document.getElementById('root')).render(
8 |
9 |
10 |
11 |
12 | ,
13 | )
14 |
--------------------------------------------------------------------------------
/client/src/pages/About/About.css:
--------------------------------------------------------------------------------
1 | main {
2 | height: 100vh;
3 | width: 100%;
4 | }
5 |
6 | section {
7 | font-family: sans-serif;
8 | font-size: 12ch;
9 | height: 100%;
10 | width: 100%;
11 | display: flex;
12 | justify-content: center;
13 | align-items: center;
14 | }
15 |
16 | section:nth-child(1) {
17 | background: #252525;
18 | color: white;
19 | }
20 |
21 | section:nth-child(2) {
22 | background: #191919;
23 | color: white;
24 | }
25 |
26 | section:nth-child(3) {
27 | background: #252525;
28 | color: white;
29 | }
30 |
31 | .about-signup-btn {
32 | color: #080710;
33 | margin-top: 30px;
34 | background-color: #ffffff;
35 | background: linear-gradient(to bottom right, #fd3302, #ff9a5a);
36 | transition: box-shadow 0.2s ease-in-out;
37 | height: 2em;
38 | }
39 |
40 | .about-signup-btn:not([disabled]):hover {
41 | box-shadow: 0 0 0.25rem rgba(0, 0, 0, 0.5),
42 | -0.125rem -0.125rem 1rem rgba(239, 71, 101, 0.5),
43 | 0.125rem 0.125rem 1rem rgba(255, 154, 90, 0.5);
44 | }
45 |
46 | .about-hero {
47 | font-size: large;
48 | color: white;
49 | display: flex;
50 | flex-direction: row;
51 | align-items: center;
52 | }
53 |
54 | .about-hero-left {
55 | flex: 1;
56 | float: left;
57 | font-size: larger;
58 | align-items: center;
59 | justify-content: center;
60 | }
61 |
62 | .about-hero-right {
63 | flex: 1;
64 | float: right;
65 | font-size: larger;
66 | align-items: center;
67 | justify-content: center;
68 | }
69 |
70 | .about-features {
71 | font-size: large;
72 | color: white;
73 | display: flex;
74 | flex-direction: row;
75 | align-items: center;
76 | }
77 |
78 | .about-features-left {
79 | float: left;
80 | font-size: larger;
81 | align-items: center;
82 | justify-content: center;
83 | }
84 |
85 | .about-features-right {
86 | float: right;
87 | font-size: larger;
88 | align-items: center;
89 | justify-content: left;
90 | text-align: left;
91 | margin-left: 5em;
92 | }
93 |
94 | .about-cta {
95 | justify-content: left;
96 | align-items: center;
97 | text-align: left;
98 | display: flex;
99 | font-size: x-large;
100 | color: white;
101 | }
--------------------------------------------------------------------------------
/client/src/pages/About/About.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { useNavigate } from 'react-router-dom';
3 | import NavBar from '../../components/NavBar';
4 | import { Link } from 'react-router-dom';
5 | import './About.css'
6 | import graphPng from './graph.png'
7 | import spinner from '../../assets/pulse-1.1s-200px.svg';
8 |
9 |
10 | const About = () => {
11 |
12 | const navigate = useNavigate();
13 |
14 |
15 | return (
16 |
17 |
18 |
19 |
20 |
21 |
22 |
LambdaPulse is your solution to monitoring AWS Lambda X-Ray Traces.
23 |
24 |
25 |
26 |
27 |
28 |

29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |

37 |
38 |
39 |
Features
40 |
41 | - Persist traces beyond X-ray's built in time horizon
42 | - View and filter traces by date & time range
43 | - Visualize traces
44 | - Find and view errors
45 | - View detailed logs per trace
46 | - Visualize system graph
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | );
70 | };
71 |
72 | export default About;
73 |
--------------------------------------------------------------------------------
/client/src/pages/About/graph.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/LambdaPulse/f6ef43b6567ed2d02946abd5edc3c129628930b2/client/src/pages/About/graph.png
--------------------------------------------------------------------------------
/client/src/pages/ComingSoon.jsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 |
3 | const name = (props) => {
4 | return (
5 |
6 |
Coming Soon!
7 |
8 | )
9 | };
10 |
11 | export default name;
12 |
--------------------------------------------------------------------------------
/client/src/pages/Dashboard/Dashboard.css:
--------------------------------------------------------------------------------
1 | #dashboardBody {
2 | display:flex;
3 | }
4 | #sideBar {
5 | width:6vw;
6 | display:flex;
7 | flex-direction: column;
8 | border-style: inset;
9 | border-color:rgb(93, 93, 93);
10 | border-width: 3px;
11 | text-align: center;
12 | }
13 | button{
14 | color:rgb(255, 51, 0);
15 | background-color: #252525
16 | }
17 | #bodyContent{
18 | width:94vw;
19 | background-color: #252525;
20 | border-style: inset;
21 | border-color:rgb(93, 93, 93);
22 | border-width: 3px;
23 | }
--------------------------------------------------------------------------------
/client/src/pages/Dashboard/Dashboard.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import NavBar from '../../components/NavBar';
3 | import EventGraph from '../../components/EventGraph';
4 | import HomeDisplay from '../../components/HomeDisplay';
5 | import TraceList from '../../components/TraceList';
6 | import Metrics from '../../components/Metrics';
7 | import Settings from '../../components/Settings';
8 | import { Route, Routes } from 'react-router-dom';
9 | import { useNavigate } from 'react-router-dom';
10 | import './Dashboard.css';
11 | import sampleTraces from '../../assets/sampleTraces.json';
12 | import ComingSoon from '../ComingSoon'
13 |
14 | import homeIcon from '../../assets/home-1391-svgrepo-com.svg';
15 | import eventGraphIcon from '../../assets/network-2-1118-svgrepo-com.svg';
16 | import traceListIcon from '../../assets/list-svgrepo-com.svg';
17 | import metricsIcon from '../../assets/chart-bar-svgrepo-com.svg';
18 | import teamIcon from '../../assets/team-svgrepo-com.svg';
19 | import settingsIcon from '../../assets/settings-svgrepo-com.svg';
20 | import mapIcon from '../../assets/map-svgrepo-com.svg'
21 | import logoutIcon from '../../assets/logout-svgrepo-com.svg';
22 |
23 | const Dashboard = () => {
24 | const [currentPage, setCurrentPage] = useState('Home');
25 | const [currentTrace, setCurrentTrace] = useState(0);
26 | const [traceList, setTraceList] = useState({});
27 | const [nodeData, setNodeData] = useState(null);
28 | const [refresh, setRefresh] = useState(false);
29 | const [loading, setLoading] = useState(false);
30 | const [appTreeNode, setAppTreeNode] = useState({});
31 | const [appLogs, setAppLogs] = useState([]);
32 | const [start_value, onChangeStart] = useState(new Date()-1000*60*60*24*7);
33 | const [end_value, onChangeEnd] = useState(new Date());
34 |
35 | const navigate = useNavigate();
36 |
37 |
38 | useEffect(() => {
39 | console.log('FIRED OFF USEFFECT');
40 | setLoading(true);
41 | fetch('/getTraces', {
42 | method: 'GET',
43 | headers: {
44 | 'Content-type': 'application/json',
45 | },
46 | })
47 | .then((result) => {
48 | if (result.status === 419) {
49 | navigate('/');
50 | }
51 | return result.json()
52 | })
53 | .then((data) => {
54 | setLoading(false);
55 | if (data.length) {
56 | setNodeData(data[currentTrace]);
57 | setTraceList(data);
58 | } else {
59 | console.log('Fetched nothing, defaulting to placeholder data');
60 | setNodeData(sampleTraces[currentTrace]);
61 | setTraceList(sampleTraces);
62 | }
63 | })
64 | .catch((err) => console.log(err));
65 | }, [refresh]);
66 |
67 | useEffect(() => {
68 | setNodeData(traceList[currentTrace]);
69 | }, [currentTrace]);
70 |
71 | useEffect(() => {
72 | //get from right
73 | const getFromRight = (s) => {
74 | let result = '';
75 | for (let i = s.length-1; i >= 0; i--) {
76 | if (s[i] == '/') return result;
77 | result = s[i] + result;
78 | }
79 | return result;
80 | }
81 | let newAppChildren = [];
82 | //loop thru tracelist get endpt
83 | for (let n = 0; n < traceList.length; n++) {
84 | const url = traceList[n].fullData.Document.http.request.url;
85 | const endpt = getFromRight(url)
86 | // let errors = findErrorsInTrace(traceList[n]);
87 |
88 | //ADDING TO APP TREE CHILDREN
89 |
90 | let found = false;
91 | // console.log(`this is trace${n} endpt`, endpt)
92 | //loop thru children get endpt
93 | for (let j = 0; j < newAppChildren.length; j++) {
94 | let appChildrenUrl = newAppChildren[j].fullData.Document.http.request.url;
95 | let appChildrenEndpt = getFromRight(appChildrenUrl)
96 | // console.log(`this is appchild${j} endpt`, appChildrenEndpt)
97 | if (appChildrenEndpt == endpt) {
98 | found = true;
99 | }
100 | }
101 | if (!found) {
102 | // console.log('shud b adding')
103 | newAppChildren.push(traceList[n]);
104 | }
105 | }
106 | console.log('this is apptreechildren',newAppChildren);
107 | let logs = [];
108 | for (let i = 0; i < newAppChildren.length; i++){
109 | logs=[...logs,...traceList[i].logs]
110 | }
111 | let client = {
112 | id :'client',
113 | name:'client',
114 | logs: logs,
115 | children: newAppChildren,
116 | origin: 'client',
117 | //fulldata, has .Document
118 | }
119 | setAppTreeNode(client);
120 | //ADDING TO APP TREE CHILDREN
121 | setAppLogs(logs);
122 | console.log('this is logs: ', logs);
123 | if (newAppChildren.length!= 0) {
124 | console.log(newAppChildren[0].fullData.Document)
125 |
126 | }
127 |
128 | }, [traceList])
129 |
130 | function logout() {
131 | fetch('/logout', {
132 | method: 'GET',
133 | headers: {
134 | 'Content-type': 'application/json',
135 | },
136 | })
137 | .then((response) => {
138 | if(response.status === 200) {
139 | navigate('/');
140 | }
141 | })
142 | }
143 |
144 | function Body() {
145 | return (
146 |
147 | {currentPage === 'Home' && (
148 |
149 | )}
150 | {currentPage === 'EventGraph' && (
151 |
155 | )}
156 | {currentPage === 'AppTree' && (
157 |
161 | )}
162 | {currentPage === 'TraceList' && (
163 |
175 | )}
176 | {currentPage === 'Metrics' && (
177 |
178 | )}
179 | {currentPage === 'Team' && (
180 |
181 | )}
182 | {currentPage === 'Settings' && (
183 |
184 | )}
185 |
186 | );
187 | }
188 | return (
189 |
222 |
223 | );
224 | };
225 |
226 | export default Dashboard;
227 |
--------------------------------------------------------------------------------
/client/src/pages/Login.css:
--------------------------------------------------------------------------------
1 | *,
2 | *:before,
3 | *:after {
4 | padding: 0;
5 | margin: 0;
6 | box-sizing: border-box;
7 | }
8 | body {
9 | background-color: #080710;
10 | }
11 | .background {
12 | width: 430px;
13 | height: 520px;
14 | position: absolute;
15 | transform: translate(-50%, -50%);
16 | left: 50%;
17 | top: 50%;
18 | }
19 | .background .shape {
20 | height: 200px;
21 | width: 200px;
22 | position: absolute;
23 | border-radius: 50%;
24 | }
25 | .shape:last-child {
26 | background: linear-gradient(#1845ad, #23a2f6);
27 | left: -80px;
28 | top: -80px;
29 | }
30 | .shape:first-child {
31 | background: linear-gradient(to right, #fd3302, #f09819);
32 | right: -30px;
33 | bottom: -80px;
34 | }
35 | .auth-form {
36 | width: 400px;
37 | background-color: rgba(255, 255, 255, 0.13);
38 | position: absolute;
39 | transform: translate(-50%, -50%);
40 | top: 50%;
41 | left: 50%;
42 | border-radius: 10px;
43 | backdrop-filter: blur(10px);
44 | border: 2px solid rgba(255, 255, 255, 0.1);
45 | box-shadow: 0 0 40px rgba(8, 7, 16, 0.6);
46 | padding: 50px 35px;
47 | }
48 |
49 | .auth-form * {
50 | color: #ffffff;
51 | letter-spacing: 0.5px;
52 | outline: none;
53 | border: none;
54 | }
55 | .auth-form h3 {
56 | font-size: 32px;
57 | font-weight: 500;
58 | line-height: 42px;
59 | text-align: center;
60 | margin-top: 0px;
61 | }
62 | .login-form {
63 | height: 450px;
64 | }
65 |
66 | .signup-form {
67 | height: 690px;
68 | margin-top: 20px;
69 | }
70 | label {
71 | text-align: left;
72 | display: block;
73 | margin-top: 15px;
74 | font-size: 16px;
75 | font-weight: 500;
76 | }
77 | input {
78 | display: block;
79 | height: 40px;
80 | width: 100%;
81 | background-color: rgba(255, 255, 255, 0.07);
82 | border-radius: 3px;
83 | padding: 0 10px;
84 | margin-top: 3px;
85 | font-size: 14px;
86 | font-weight: 300;
87 | }
88 | ::placeholder {
89 | color: #e5e5e5;
90 | }
91 | .login-btn {
92 | color: #080710;
93 | margin-top: 30px;
94 | background-color: #ffffff;
95 | background: linear-gradient(to bottom right, #fd3302, #ff9a5a);
96 | transition: box-shadow 0.2s ease-in-out;
97 | }
98 |
99 | .login-btn:not([disabled]):hover {
100 | box-shadow: 0 0 0.25rem rgba(0, 0, 0, 0.5),
101 | -0.125rem -0.125rem 1rem rgba(239, 71, 101, 0.5),
102 | 0.125rem 0.125rem 1rem rgba(255, 154, 90, 0.5);
103 | }
104 |
105 | .btn-login {
106 | color: #080710;
107 | margin-top: 30px;
108 | background-color: #f7f7f7;
109 | transition: box-shadow 0.2s ease-in-out;
110 | }
111 | .btn-login:not([disabled]):hover {
112 | box-shadow: 0 0 0.25rem rgba(0, 0, 0, 0.5),
113 | -0.125rem -0.125rem 1rem rgba(255, 180, 120, 0.5),
114 | 0.125rem 0.125rem 1rem rgba(255, 180, 120, 0.5);
115 | }
116 | .already-have-account {
117 | margin-top: 10px;
118 | margin-bottom: 5px;
119 | }
120 | button {
121 | width: 100%;
122 | background-color: #ffffff;
123 | color: #080710;
124 | padding: 15px 0;
125 | font-size: 18px;
126 | font-weight: 600;
127 | border-radius: 5px;
128 | cursor: pointer;
129 | }
130 |
131 | .error-message {
132 | color: #f09819;
133 | }
134 |
135 | .captcha {
136 | margin-top: 20px;
137 | }
138 |
--------------------------------------------------------------------------------
/client/src/pages/Login.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { useNavigate } from 'react-router-dom';
3 | import NavBar from '../components/NavBar';
4 | import { Link } from 'react-router-dom';
5 | import { getApiBaseUrl } from '../apiBaseUrl.js';
6 |
7 | import './Login.css';
8 |
9 | const Login = () => {
10 | const [email, setEmail] = useState('');
11 | const [password, setPassword] = useState('');
12 | const [errorMessage, setErrorMessage] = useState('');
13 |
14 | const navigate = useNavigate();
15 |
16 | const verifyLogin = (e) => {
17 | const userData = {
18 | email,
19 | password,
20 | };
21 | fetch(`${getApiBaseUrl()}/verifyUser`, {
22 | method: 'POST',
23 | headers: { 'Content-type': 'application/json' },
24 | body: JSON.stringify(userData),
25 | })
26 | .then((response) => {
27 | if (response.status === 200) {
28 | navigate('/dashboard');
29 | } else if (response.status === 401) {
30 | setErrorMessage('Invalid password or email');
31 | }
32 | })
33 | .catch((err) => {
34 | console.log('Error:', err);
35 | });
36 | };
37 | return (
38 |
82 | );
83 | };
84 |
85 | export default Login;
86 |
--------------------------------------------------------------------------------
/client/src/pages/Signup.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { useNavigate } from 'react-router-dom';
3 | import NavBar from '../components/NavBar';
4 | import Reaptcha from 'reaptcha';
5 | import './Login.css';
6 |
7 | const Signup = () => {
8 | const [email, setEmail] = useState('');
9 | const [password, setPassword] = useState('');
10 | const [fullName, setFulltName] = useState('');
11 | const [confirmPassword, setConfirmPassword] = useState('');
12 | const [errorMessage, setErrorMessage] = useState('');
13 | const [captcha, setCaptcha] = useState('');
14 |
15 | const navigate = useNavigate();
16 |
17 | const handleSignup = (e) => {
18 | if (password !== confirmPassword) {
19 | setErrorMessage('Passwords do not match');
20 | return;
21 | }
22 | if (captcha !== 'passed') {
23 | setErrorMessage('Captcha required');
24 | return;
25 | }
26 |
27 | const userData = {
28 | email,
29 | password,
30 | fullName,
31 | };
32 |
33 | fetch('/createUser', {
34 | method: 'POST',
35 | headers: { 'Content-type': 'application/json' },
36 | body: JSON.stringify(userData),
37 | })
38 | .then((response) => {
39 | console.log(response);
40 | if (response.status === 201) {
41 | navigate('/dashboard');
42 | } else if (response.status === 409) {
43 | setErrorMessage('Email already exists');
44 | }
45 | })
46 | .catch((error) => {
47 | console.log('Error:', error);
48 | });
49 | };
50 |
51 | return (
52 |
123 | );
124 | };
125 | export default Signup;
126 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.9'
2 | services:
3 | prod:
4 | build: .
5 | container_name: lambdapulse-prod
6 | ports:
7 | - '80:3000'
8 | networks:
9 | - lambdapulse-network
10 | volumes:
11 | - ./:/usr/src/app
12 | env_file:
13 | - .env
14 | depends_on:
15 | - redis
16 | command: npm run serve
17 |
18 | redis:
19 | container_name: redis
20 | image: redis:latest
21 | command: ['redis-server', '--bind', 'redis', '--port', '6379']
22 | restart: always
23 | environment:
24 | - ALLOW_EMPTY_PASSWORD=yes
25 | ports:
26 | - '6379:6379'
27 | networks:
28 | - lambdapulse-network
29 |
30 | networks:
31 | lambdapulse-network:
32 | driver: bridge
33 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | LambdaPulse
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "osp7project",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "concurrently \"cd ./server && npm run server\" \"cd && npm run dev\"",
8 | "server": "node server/server.js",
9 | "fulldev": " NODE_ENV=DEV concurrently \"redis-server\" \"npx nodemon server/server.js\" \" npx vite \"",
10 | "dev": "vite",
11 | "build": "vite build",
12 | "preview": "vite preview",
13 | "test": "vite build --mode test && vitest",
14 | "test:coverage": "vitest --coverage",
15 | "serve": "concurrently \"npm run server\"",
16 | "fullprod": "concurrently \"redis-server\" \"npx nodemon server/server.js\" \"cd ./client/dist && http-server\"",
17 | "redis-test": "node redis_test.js"
18 | },
19 | "keywords": [],
20 | "author": "",
21 | "license": "ISC",
22 | "dependencies": {
23 | "@aws-sdk/client-cloudwatch-logs": "^3.316.0",
24 | "@aws-sdk/client-dynamodb": "^3.315.0",
25 | "@aws-sdk/client-xray": "^3.312.0",
26 | "@aws-sdk/lib-dynamodb": "^3.315.0",
27 | "@aws-sdk/util-dynamodb": "^3.315.0",
28 | "aws-sdk": "^2.1354.0",
29 | "axios": "^1.3.5",
30 | "bcrypt": "^5.1.0",
31 | "concurrently": "^8.0.1",
32 | "cookie-parser": "^1.4.6",
33 | "cors": "^2.8.5",
34 | "d3": "^7.8.4",
35 | "dotenv": "^16.0.3",
36 | "express": "^4.18.2",
37 | "install": "^0.13.0",
38 | "ioredis": "^5.3.2",
39 | "jest": "^29.5.0",
40 | "jsonwebtoken": "^9.0.0",
41 | "nodemon": "^2.0.22",
42 | "npm": "^9.6.4",
43 | "pg": "^8.10.0",
44 | "react": "^18.2.0",
45 | "react-d3-tree": "^3.5.1",
46 | "react-data-table-component": "^7.5.3",
47 | "react-datepicker": "^4.11.0",
48 | "react-dom": "^18.2.0",
49 | "react-minimal-pie-chart": "^8.4.0",
50 | "react-router-dom": "^6.10.0",
51 | "reaptcha": "^1.12.1",
52 | "redis": "^4.6.6",
53 | "supertest": "^6.3.3",
54 | "uuid": "^9.0.0"
55 | },
56 | "devDependencies": {
57 | "@testing-library/jest-dom": "^5.16.5",
58 | "@testing-library/react": "^14.0.0",
59 | "@testing-library/user-event": "^14.4.3",
60 | "@types/react": "^18.0.28",
61 | "@types/react-dom": "^18.0.11",
62 | "@vitejs/plugin-react": "^3.1.0",
63 | "@vitest/coverage-c8": "^0.30.1",
64 | "msw": "^1.2.1",
65 | "playwright": "^1.33.0",
66 | "vite": "^4.2.0",
67 | "vitest": "^0.30.1"
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/server/aws_sdk/sortingSegments.js:
--------------------------------------------------------------------------------
1 | //parse JSON data within a given array of segments
2 | const parseData = (segmentArray) => {
3 | const parsedDocumentData = segmentArray.map((segments) => {
4 | const doc = JSON.parse(segments['Document']);
5 | segments['Document'] = doc;
6 | return segments;
7 | });
8 | return parsedDocumentData;
9 | };
10 |
11 | //create node. adjust data as needed
12 | function Node(segment) {
13 | this.id = segment.Document.id;
14 | this.name = segment.Document.name;
15 | this.parent_id = segment.Document.parent_id;
16 | this.time_taken = Math.round(
17 | (segment.Document.end_time - segment.Document.start_time) * 1000
18 | );
19 | this.subsegments = segment.Document.subsegments;
20 | this.children = [];
21 | this.origin = segment.Document.origin;
22 | this.http = segment.Document.http;
23 | this.fullData = segment;
24 | }
25 |
26 | // create individual node for an array of related segments
27 | const createAllNodes = (segmentarray) => {
28 | return segmentarray.map((segment) => {
29 | return new Node(segment);
30 | });
31 | };
32 |
33 | // create root node
34 | const createRoot = (arr) => {
35 | let rootNode;
36 | for (let i = 0; i < arr.length; i++) {
37 | if (!arr[i].parent_id) {
38 | rootNode = arr[i];
39 | }
40 | }
41 | return rootNode;
42 | };
43 |
44 | // create parent/child relationship from an array of nodes starting from root
45 | const addChildChildren = (tree, nodeArray) => {
46 | let currNode = tree;
47 | if (!currNode) return;
48 | nodeArray.forEach((node) => {
49 | if (node.parent_id === currNode.id) {
50 | currNode.children.push(node);
51 | } else if (currNode.subsegments) {
52 | currNode.subsegments.forEach((subSegment) => {
53 | if (node.parent_id === subSegment.id) {
54 | currNode.children.push(node);
55 | }
56 | if (subSegment.subsegments) {
57 | if (checkForSubsegments(node, subSegment.subsegments) === true) {
58 | currNode.children.push(node);
59 | }
60 | }
61 | });
62 | }
63 | });
64 | for (let i = 0; i < currNode.children.length; i++) {
65 | addChildChildren(currNode.children[i], nodeArray);
66 | }
67 | };
68 |
69 | // checks nodes subsegments for relationship between nodes if subsegment exists
70 | const checkForSubsegments = (node, subsegment) => {
71 | for (let i = 0; i < subsegment.length; i++) {
72 | if (node.parent_id === subsegment[i].id) return true;
73 | if (subsegment[i].subsegments) {
74 | const result = checkForSubsegments(node, subsegment[i].subsegments);
75 | if (result) return true;
76 | }
77 | }
78 | };
79 |
80 | const getAverageOfTrace = (nodeArray) => {
81 | let total = 0;
82 | nodeArray.forEach((node) => {
83 | total += node.time_taken;
84 | });
85 | return total / nodeArray.length;
86 | };
87 |
88 | const main = (segment) => {
89 | if (!segment) return;
90 | const segmentData = parseData(segment);
91 | const arrayOfSegmentNodes = createAllNodes(segmentData);
92 | const averageTimeTaken = getAverageOfTrace(arrayOfSegmentNodes);
93 | const root = createRoot(arrayOfSegmentNodes);
94 | root.averageTime = averageTimeTaken;
95 | addChildChildren(root, arrayOfSegmentNodes);
96 | return [root, segmentData];
97 | };
98 |
99 | module.exports = main;
100 |
--------------------------------------------------------------------------------
/server/aws_sdk/traceDetails.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 | const { query } = require('../db.config.js');
3 | const Redis = require('ioredis');
4 |
5 | const {
6 | XRayClient,
7 | GetTraceSummariesCommand,
8 | BatchGetTracesCommand,
9 | } = require('@aws-sdk/client-xray');
10 | const {
11 | CloudWatchLogsClient,
12 | FilterLogEventsCommand,
13 | } = require('@aws-sdk/client-cloudwatch-logs');
14 |
15 | let redisClient;
16 |
17 | if (process.env.NODE_ENV === 'DEV' || process.env.NODE_ENV === 'TEST') {
18 | console.log('devmode in rediscontroller');
19 | redisClient = new Redis();
20 | } else {
21 | console.log('not devmode in rediscontroller');
22 | redisClient = new Redis({
23 | host: 'redis',
24 | port: 6379,
25 | });
26 | }
27 | redisClient.on('connect', () => {
28 | console.log('Connected to Redis.');
29 | });
30 |
31 | redisClient.on('error', (err) => {
32 | console.error(err);
33 | });
34 |
35 | const main = require('./sortingSegments');
36 |
37 | const getTraceMiddleware = {
38 | getSummary: async (req, res, next) => {
39 | if (res.locals.redisTraces != undefined) return next();
40 | console.log('in getTraceMiddleware');
41 |
42 | const xClient = new XRayClient({
43 | credentials: res.locals.awsCredentials,
44 | region: 'us-east-1',
45 | });
46 | const getTraceSummary = async () => {
47 | console.log('in getTracesummary');
48 | const endTime = new Date();
49 | const startTime = new Date(endTime.getTime() - 24 * 60 * 60 * 1000);
50 |
51 | const params = {
52 | StartTime: startTime,
53 | EndTime: endTime,
54 | TimeRangeType: 'TraceId',
55 | };
56 |
57 | const response = await xClient.send(new GetTraceSummariesCommand(params));
58 |
59 | return response;
60 | };
61 | // get the data in xray. will return an array on res
62 | try {
63 | const result = await getTraceSummary();
64 | const traceArray = result.TraceSummaries;
65 | const traceIds = traceArray.map((node) => {
66 | // console.log(node);
67 | // get arn for each node
68 | // console.log(node.ResourceARNs[1]);
69 | // console.log(node.Id);
70 | return node.Id;
71 | });
72 | // console.log(traceIds);
73 | res.locals.traceArray = traceIds;
74 | next();
75 | } catch (err) {
76 | next(err);
77 | }
78 | },
79 |
80 | // get segment data
81 | getSegmentArray: async (req, res, next) => {
82 | if (res.locals.redisTraces != undefined) return next();
83 | console.log('in getSegmentArray');
84 | const xClient = new XRayClient({
85 | credentials: res.locals.awsCredentials,
86 | region: 'us-east-1',
87 | });
88 | const getTraceDetails = async (traceIds) => {
89 | const params = {
90 | TraceIds: traceIds,
91 | };
92 |
93 | const response = await xClient.send(new BatchGetTracesCommand(params));
94 | return response;
95 | };
96 | try {
97 | let fullTraceArray = [];
98 |
99 | let currTraceIds = [];
100 | while (res.locals.traceArray.length) {
101 | if (currTraceIds.length < 5)
102 | currTraceIds.push(res.locals.traceArray.shift());
103 | else {
104 | const result = await getTraceDetails(currTraceIds);
105 | fullTraceArray = fullTraceArray.concat(result.Traces);
106 | currTraceIds = [];
107 | }
108 | }
109 | if (currTraceIds.length > 0) {
110 | const result = await getTraceDetails(currTraceIds);
111 | fullTraceArray = fullTraceArray.concat(result.Traces);
112 | }
113 | console.log(fullTraceArray, 'this is full trace array');
114 | res.locals.traceSegmentData = fullTraceArray;
115 | next();
116 | } catch (err) {
117 | next({ log: err });
118 | }
119 | },
120 |
121 | sortSegments: async (req, res, next) => {
122 | if (res.locals.redisTraces != undefined) {
123 | res.locals.userTraces = res.locals.redisTraces;
124 | return next();
125 | }
126 | console.log('in sortedSegments');
127 | try {
128 | const allNodes = [];
129 | // traceIds can be found at the element
130 |
131 | for (let i = 0; i < res.locals.traceSegmentData.length; i++) {
132 | const currSegment = res.locals.traceSegmentData[i].Segments;
133 | // console.log(currSegment);
134 | const currRoot = main(currSegment);
135 |
136 | // below is the process to get the logs for the lambda functions
137 | // currRoot[0] is the node
138 | let currentAllSegments = currRoot[1];
139 |
140 | if (currentAllSegments.length) {
141 | for (let i = 0; i < currentAllSegments.length; i++) {
142 | if (
143 | currentAllSegments[i].Document.origin === 'AWS::Lambda' &&
144 | currentAllSegments[i].Document.aws.request_id
145 | ) {
146 | let requestId = currentAllSegments[i].Document.aws.request_id;
147 | let segmentName = `/aws/lambda/${currentAllSegments[i].Document.name}`;
148 | // await getLogs();
149 | console.log(requestId, ' ', segmentName);
150 |
151 | // call the functino for get logs in here and add the to the node
152 | // add the logs onto currRoot[0].logs = logs or something
153 |
154 | const logs = await getLogs(requestId, segmentName);
155 |
156 | logs.forEach((log) => {
157 | if (log.message.includes('START')) {
158 | currRoot[0].cold_start = true;
159 | }
160 | });
161 | currRoot[0].logs = logs;
162 | }
163 | }
164 | }
165 | allNodes.push(currRoot[0]);
166 | }
167 |
168 | async function getLogs(requestId, logGroupName) {
169 | const endTime = new Date();
170 | const startTime = new Date(endTime.getTime() - 60 * 60 * 1000);
171 |
172 | const cloudwatchlogs = new CloudWatchLogsClient({
173 | credentials: res.locals.awsCredentials,
174 | region: 'us-east-1',
175 | });
176 |
177 | const params = {
178 | logGroupName,
179 |
180 | startTime: startTime.getTime(),
181 | endTime: endTime.getTime(),
182 |
183 | // filter:
184 | };
185 |
186 | const command = new FilterLogEventsCommand(params);
187 |
188 | try {
189 | const data = await cloudwatchlogs.send(command);
190 | console.log(data, 'this is the data');
191 | const node_logs = data.events.filter((segEvent) => {
192 | return segEvent.message.includes(requestId);
193 | });
194 | return node_logs;
195 | } catch (error) {
196 | console.error('Error fetching logs:', error);
197 | }
198 | }
199 |
200 | // save all nodes to res.locals
201 | res.locals.nodes = allNodes;
202 |
203 | // grab current userId to send a query to the DB
204 | const userId = res.locals.userId;
205 |
206 | // inserting new traces into traces table. On conflict (when traces already in DB) does nothing
207 | try {
208 | const insertTraceQuery = `
209 | INSERT INTO traces (_id, root_node, role_arn)
210 | VALUES ($1, $2, (SELECT role_arn FROM users WHERE _id = $3))
211 | ON CONFLICT (_id) DO NOTHING RETURNING *;
212 | `;
213 | for (let i = 0; i < allNodes.length; i++) {
214 | const rootNode = allNodes[i];
215 | const traceId = rootNode.id;
216 | console.log(rootNode.fullData.Document.start_time);
217 | const resultTraces = await query(insertTraceQuery, [
218 | traceId,
219 | JSON.stringify(rootNode),
220 | userId,
221 | ]);
222 | }
223 |
224 | // deletes traces older than 7 days
225 | const deleteOldTracesQuery = `
226 | DELETE FROM traces
227 | WHERE role_arn = (SELECT role_arn FROM users WHERE _id = $1)
228 | AND ((root_node -> 'fullData' -> 'Document' ->> 'start_time')::
229 | double precision < EXTRACT(EPOCH FROM (NOW() - INTERVAL '7 days')));`;
230 | await query(deleteOldTracesQuery, [userId]);
231 | } catch (err) {
232 | console.log('error', err);
233 | next(err);
234 | }
235 |
236 | // Selecting traces, sorting them in descending order and passing on to res.locals
237 | try {
238 | const selectTracesQuery = `
239 | SELECT t.root_node
240 | FROM traces t
241 | JOIN users u ON t.role_arn = u.role_arn
242 | WHERE u._id = $1
243 | ORDER BY (t.root_node -> 'fullData' -> 'Document' ->> 'start_time')::double precision DESC;
244 | `;
245 | const tracesResult = await query(selectTracesQuery, [userId]);
246 |
247 | const userTraces = tracesResult.rows.map((row) => row.root_node);
248 | console.log('this is userTraces', userTraces);
249 | res.locals.userTraces = userTraces;
250 | } catch (err) {
251 | console.log('error', err);
252 | next(err);
253 | }
254 |
255 | try {
256 | redisClient.set('Traces', JSON.stringify(res.locals.userTraces));
257 | } catch (err) {
258 | next(err);
259 | }
260 | next();
261 | } catch (err) {
262 | next(err);
263 | }
264 | },
265 | };
266 |
267 | module.exports = getTraceMiddleware;
268 |
--------------------------------------------------------------------------------
/server/controllers/awsCredentialsController.js:
--------------------------------------------------------------------------------
1 | const { AssumeRoleCommand } = require('@aws-sdk/client-sts');
2 | const { stsClient } = require('../db.config.js');
3 | const { query } = require('../db.config.js');
4 |
5 | const awsCredentialsController = {};
6 |
7 | // retrieve user's AWS Role ARN from the database.
8 | awsCredentialsController.getCredentials = async (req, res, next) => {
9 | console.log('in getCredentials');
10 | if (res.locals.redisTraces != undefined) {
11 | return next();
12 | }
13 | console.log('Received creds request at ' + Date.now());
14 |
15 | const roleResult = await query('SELECT role_arn FROM users WHERE _id = $1 ;', [res.locals.userId])
16 | const getRole = roleResult.rows[0].role_arn
17 |
18 | const userRoleArn = getRole;
19 | console.log('req body: ', req.body);
20 |
21 | if (!userRoleArn) {
22 | return res.status(400).json({ message: 'User Role ARN is required.' });
23 | }
24 |
25 | const assumeRoleParams = {
26 | RoleArn: userRoleArn,
27 | RoleSessionName: 'lambdaPulseSession',
28 | };
29 | // assume the role specified by the Role ARN, create temp credentials
30 | try {
31 | const data = await stsClient.send(new AssumeRoleCommand(assumeRoleParams));
32 | const temporaryCredentials = {
33 | accessKeyId: data.Credentials.AccessKeyId,
34 | secretAccessKey: data.Credentials.SecretAccessKey,
35 | sessionToken: data.Credentials.SessionToken,
36 | };
37 | console.log('in awsCredentialsController');
38 | res.locals.awsCredentials = temporaryCredentials;
39 | return next();
40 | } catch (err) {
41 | console.error('Error assuming role:', err);
42 | let error = {
43 | log: 'Express error handler caught awsCredentialsController.getCredentials',
44 | message: { err: err },
45 | };
46 | return next(error);
47 | }
48 | };
49 |
50 | module.exports = awsCredentialsController;
51 |
--------------------------------------------------------------------------------
/server/controllers/jwtController.js:
--------------------------------------------------------------------------------
1 | const jwt = require("jsonwebtoken");
2 |
3 |
4 | const jwtController = {
5 | createJwt: (req,res,next) => {
6 | console.log("increate jwt")
7 | try {
8 | const token = jwt.sign({id:res.locals.userId}, process.env.JWT_KEY, {expiresIn:"1h"});
9 | res.cookie("token", token, {
10 | httpOnly:true,
11 | // secure: true,
12 | // maxAge: 100000,
13 | // signed: true,
14 | })
15 | return next();
16 | } catch(err) {
17 | console.log('Error', err);
18 | let error = {
19 | log: 'Express error handler caught jwtController.createJwt',
20 | message: { err: err },
21 | };
22 | return next(error);
23 | }
24 | },
25 | verifyJwt: (req,res,next) => {
26 | console.log('in verifyjwt')
27 | console.log('this is req.cookies',req.cookies);
28 | const token = req.cookies.token;
29 | console.log(token);
30 | // console.log("this is req", req);
31 | try {
32 | const user = jwt.verify(token,process.env.JWT_KEY);
33 | console.log(user + ' connected')
34 | const userId = user.id;
35 | res.locals.userId = userId;
36 | return next();
37 | } catch(err) {
38 | res.clearCookie("token");
39 | res.sendStatus(419);
40 | }
41 | },
42 | }
43 | module.exports = jwtController;
--------------------------------------------------------------------------------
/server/controllers/redisController.js:
--------------------------------------------------------------------------------
1 | const Redis = require('ioredis');
2 |
3 | let redisClient;
4 |
5 | if (process.env.NODE_ENV === 'DEV' || process.env.NODE_ENV === 'TEST') {
6 | console.log('devmode in rediscontroller');
7 | redisClient = new Redis();
8 | } else {
9 | console.log('not devmode in rediscontroller');
10 | redisClient = new Redis({
11 | host: 'redis',
12 | port: 6379,
13 | });
14 | }
15 | redisClient.on('connect', () => {
16 | console.log('Connected to Redis.');
17 | });
18 |
19 | redisClient.on('error', (err) => {
20 | console.error(err);
21 | });
22 |
23 | const getRedisTraces = async (req, res, next) => {
24 | //Set TableName
25 | const TableName = 'Traces';
26 | //Pull in logs from redis
27 | try {
28 | let data = await redisClient.get(TableName);
29 | if (data == null) {
30 | console.log('REDIS CACHE MISS');
31 | return next();
32 | } else {
33 | console.log('REDIS CACHE HIT');
34 | res.locals.redisTraces = JSON.parse(data);
35 | return next();
36 | }
37 | } catch (err) {
38 | console.log('Error', err);
39 | let error = {
40 | log: 'Express error handler caught redisController.getErrLogs',
41 | message: { err: err },
42 | };
43 | return next(error);
44 | }
45 | };
46 |
47 | const clearTraces = async (req, res, next) => {
48 | //Set TableName
49 | console.log('in clearTraces');
50 | const TableName = 'Traces';
51 | //Pull in logs from redis
52 | try {
53 | let data = await redisClient.del(TableName);
54 | return next();
55 | } catch (err) {
56 | console.log('Error', err);
57 | let error = {
58 | log: 'Express error handler caught redisController.clearTraces',
59 | message: { err: err },
60 | };
61 | return next(error);
62 | }
63 | };
64 |
65 | module.exports = { getRedisTraces, clearTraces };
66 |
--------------------------------------------------------------------------------
/server/controllers/userController.js:
--------------------------------------------------------------------------------
1 | const bcrypt = require('bcrypt');
2 | const { query } = require('../db.config.js');
3 | const jwt = require('jsonwebtoken');
4 |
5 | // create user using PostgresSQL
6 |
7 | const createUser = async (req, res, next) => {
8 | const { fullName, email, password } = req.body;
9 | console.log('in create user');
10 | try {
11 | const getResult = await query('SELECT * FROM users WHERE email = $1', [
12 | email,
13 | ]);
14 | if (getResult.rows.length > 0) {
15 | return res.sendStatus(409);
16 | }
17 | const salt = await bcrypt.genSalt(10);
18 | const hashedPassword = await bcrypt.hash(password, salt);
19 |
20 | const result = await query(
21 | 'INSERT INTO users (full_name, email, password) VALUES ($1, $2, $3) RETURNING _id',
22 | [fullName, email, hashedPassword]
23 | );
24 | console.log('result.rows[0].id', result.rows[0]._id);
25 | const userId = result.rows[0]._id;
26 |
27 | console.log('user created successfully');
28 | res.locals.userId = userId;
29 |
30 | return next();
31 | } catch (err) {
32 | console.log('Error', err);
33 | let error = {
34 | log: 'Express error handler caught userController.createUser',
35 | message: { err: err },
36 | };
37 | return next(error);
38 | }
39 | };
40 |
41 | // verify user using PostgresSQL
42 |
43 | const verifyUser = async (req, res, next) => {
44 | const { email, password } = req.body;
45 |
46 | try {
47 | const { rows } = await query('SELECT * FROM users WHERE email = $1', [
48 | email,
49 | ]);
50 |
51 | if (rows.length === 0) {
52 | return res.sendStatus(401);
53 | }
54 | const userData = rows[0];
55 | console.log('user id is :', userData._id);
56 | console.log('userdata', userData);
57 | const isMatch = await bcrypt.compare(password, userData.password);
58 | if (!isMatch) {
59 | return res.sendStatus(401);
60 | }
61 | res.locals.userId = userData._id;
62 | return next();
63 | } catch (err) {
64 | console.log('Error', err);
65 | let error = {
66 | log: 'Express error handler caught userController.verifyUser',
67 | message: { err: err },
68 | };
69 | return next(error);
70 | }
71 | };
72 |
73 | const logout = (req, res, next) => {
74 | try {
75 | res.clearCookie('token');
76 | res.sendStatus(200);
77 | } catch (err) {
78 | console.log('Error', err);
79 | let error = {
80 | log: 'Express error handler caught userController.verifyUser',
81 | message: { err: err },
82 | };
83 | return next(error);
84 | }
85 | };
86 |
87 | module.exports = { createUser, verifyUser, logout };
88 |
--------------------------------------------------------------------------------
/server/db.config.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 |
3 | const { Pool } = require('pg');
4 |
5 | const { STSClient } = require('@aws-sdk/client-sts');
6 |
7 | const PG_URI = process.env.PG_URI;
8 |
9 | // create a new pool here using the connection string above
10 | const pool = new Pool({
11 | connectionString: PG_URI,
12 | });
13 |
14 | // create credentials variable from .env
15 | const region = 'us-east-1';
16 | const credentials = {
17 | accessKeyId: process.env.AWS_ACCESS_KEY_ID,
18 | secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
19 | };
20 |
21 | // STSClient to be used later on to get assess role over user's account and get temp creds
22 | const stsClient = new STSClient({ region, credentials });
23 |
24 | module.exports = {
25 | stsClient,
26 | query: (text, params, callback) => {
27 | console.log('executed query', text);
28 | return pool.query(text, params, callback);
29 | },
30 | };
31 |
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const PORT = process.env.PORT || '3000';
3 | app = express();
4 | const cors = require('cors');
5 | const userController = require('./controllers/userController');
6 | const redisController = require('./controllers/redisController');
7 | const awsCredentialsController = require('./controllers/awsCredentialsController');
8 | const getTraceMiddleware = require('./aws_sdk/traceDetails');
9 | const jwtController = require('./controllers/jwtController');
10 | const cookieParser = require('cookie-parser');
11 | const { query } = require('./db.config.js');
12 | const path = require('path');
13 |
14 | app.use(express.static(path.join(__dirname, '../dist')));
15 |
16 | app.use(cors());
17 | app.use(express.json());
18 | app.use(express.urlencoded({ extended: true }));
19 | app.use(cookieParser());
20 |
21 | // handle CORS
22 | app.use(function (req, res, next) {
23 | res.header('Access-Control-Allow-Origin', '*');
24 | res.header(
25 | 'Access-Control-Allow-Headers',
26 | 'Origin, X-Requested-With, Content-Type, Accept'
27 | );
28 | res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, DELETE');
29 | next();
30 | });
31 |
32 | // route to create user in DB
33 | app.post(
34 | '/createUser',
35 | userController.createUser,
36 | jwtController.createJwt,
37 | (req, res) => {
38 | res.sendStatus(201);
39 | }
40 | );
41 |
42 | // route to verify user in DB and create JWT
43 | app.post(
44 | '/verifyUser',
45 | userController.verifyUser,
46 | jwtController.createJwt,
47 | (req, res) => {
48 | res.sendStatus(200);
49 | }
50 | );
51 |
52 | // route to logout
53 | app.get('/logout', redisController.clearTraces, userController.logout);
54 |
55 | // route to retrieve user's ARN from DB
56 | app.get('/getCurrentArn', jwtController.verifyJwt, async (req, res) => {
57 | const currentArn = await query(
58 | 'SELECT role_arn FROM users WHERE _id = $1 ; ',
59 | [res.locals.userId]
60 | );
61 | res.status(200).send(currentArn);
62 | });
63 |
64 | // route to set user ARN in DB
65 | app.post('/setUserARN', jwtController.verifyJwt, async (req, res) => {
66 | console.log('in Set User ARN');
67 | const { userARN } = req.body;
68 | const userId = res.locals.userId;
69 | console.log(userId);
70 | console.log(userId);
71 | try {
72 | await query('UPDATE users SET role_arn = $1 WHERE _id = $2 ;', [
73 | userARN,
74 | userId,
75 | ]);
76 | } catch (err) {
77 | console.log('Error setting roleARN', err);
78 | }
79 | res.status(200).send({ success: 'User ARN successfully added!' });
80 | });
81 |
82 |
83 | // route to get the temp credentials, grab traces from SDK and pass to frontend
84 | app.get(
85 | '/getTraces',
86 | jwtController.verifyJwt,
87 | redisController.getRedisTraces,
88 | awsCredentialsController.getCredentials,
89 | getTraceMiddleware.getSummary,
90 | getTraceMiddleware.getSegmentArray,
91 | getTraceMiddleware.sortSegments,
92 | (req, res) => {
93 | console.log('Sending this back to the frontend:', res.locals.userTraces);
94 | res.status(200).json(res.locals.userTraces);
95 | }
96 | );
97 |
98 | // routes to access pages on refresh or through link
99 | app.get('/signup', (req, res) => {
100 | res.sendFile(path.join(__dirname, '../dist', 'index.html'));
101 | });
102 | app.get('/dashboard', (req, res) => {
103 | res.sendFile(path.join(__dirname, '../dist', 'index.html'));
104 | });
105 | app.get('/about', (req, res) => {
106 | res.sendFile(path.join(__dirname, '../dist', 'index.html'));
107 | });
108 |
109 | //Route not found
110 | app.use((req, res, err) => {
111 | console.log(err);
112 | res.sendStatus(404);
113 | res.sendStatus(404);
114 | });
115 |
116 | //Global error handler
117 | app.use((err, req, res, next) => {
118 | const defaultErr = {
119 | log: 'Express error handler caught unknown middleware error',
120 | status: 400,
121 | message: { err: 'An error occurred' },
122 | };
123 | const errorObj = Object.assign({}, defaultErr, err);
124 | console.log('Global error: ', errorObj.log);
125 | return res.status(errorObj.status).json(errorObj.message);
126 | });
127 |
128 | app.listen(PORT, () => console.log(`Listening on port ${PORT}`));
129 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import react from '@vitejs/plugin-react';
3 | import dotenv from 'dotenv';
4 | import path from 'path';
5 |
6 | dotenv.config({ path: path.resolve(__dirname, './.env') });
7 |
8 | // https://vitejs.dev/config/
9 | export default defineConfig(({ mode }) => {
10 |
11 | const config = {
12 | plugins: [react()],
13 | define: {
14 | captchaKey: `"${process.env.VITE_CAPTCHA_KEY}"`,
15 | },
16 | test: {
17 | globals: true,
18 | environment: 'jsdom',
19 | },
20 | };
21 |
22 | config.server = {
23 | host: true,
24 | // port: 3000,
25 | proxy: {
26 | '/getTraces': 'http://localhost:3000/',
27 | '/clearTraces': 'http://localhost:3000/',
28 | '/api': 'http://localhost:3000/',
29 | '/createUser': 'http://localhost:3000/',
30 | '/verifyUser': {
31 | target: 'http://localhost:3000',
32 | changeOrigin: true,
33 | },
34 | '/logout': 'http://localhost:3000/',
35 | '/setUserARN': 'http://localhost:3000/',
36 | '/getCurrentArn': 'http://localhost:3000/',
37 | },
38 | };
39 | return config;
40 | });
41 |
--------------------------------------------------------------------------------