33 |
34 |
35 |
36 |
37 |
38 | ## About Astro
39 |
40 | Serverless architecture is an exciting mainstay of cloud computing. Amazon's Web Services (AWS) Lambda is a giant in the serverless space and is widely utilized by various companies. Its event-driven paradigm to building distributed, on-demand infrastructure is also cost-effective since Lambda functions are billed only when they are executed. This reduces the physical need for servers, eliminating expensive hosting costs just to keep a server running even if it’s not in use. One issue is that navigating through the AWS console can be daunting and frustrating. Specifically, to measure a user's lambda functions, there are too many options and this massive flexibility proves cumbersome when one only needs to visualize specific metrics at a glance.
41 |
42 | As a way to solve this, we built Astro: a free, open-source lambda function monitoring tool that users can connect to their AWS account to securely and easily monitor and track key metrics.
43 |
44 |
62 |
63 |
64 |
65 | ## Getting Started
66 |
67 | If you are a developer trying to add/improve functionality, you can skip step 4 and go to step 5. If you are an AWS end user, do not worry about step 5.
68 |
69 | 1. Fork and clone the forked repo
70 |
71 | ```sh
72 | git clone
73 | cd Astro
74 | ```
75 |
76 | 2. Install package devDependencies
77 |
78 | ```sh
79 | npm install
80 | ```
81 |
82 | 3. If you are an AWS End User then use the following command to build the application and the necessary .env template file, which you should fill in with your AWS credentials (region, security key id, and access key id).
83 |
84 |
85 |
86 |
87 |
88 |
89 | ```sh
90 | npm run build
91 | ```
92 |
93 | 4. Afterwards, you can run Astro by using the following command and then navigating to localhost:1111 in your browser
94 |
95 | ```sh
96 | npm run start
97 | ```
98 |
99 | 5. If you are a developer trying to add/improve functionality, instead of step 4 you should use the following command to run Astro in development and navigate to localhost:8080 in your browser to take advantage of hot module reloading.
100 |
101 | ```sh
102 | npm run dev
103 | ```
104 |
105 | ### Lambda Metrics
106 |
107 | The key AWS Lambda function metrics we focused on are: throttles, invocations, and errors. One can see their total metric values in Account Totals. To see metrics by function, click the Functions tab to see a list of your lambda functions and the associated metrics for each function. Within the function tab, users can visualize their metrics over a specific time period using the drop down menu. This will also update the account total metrics in the account total tab.
108 |
109 |
115 |
116 |
117 |
118 | ## Contributing
119 |
120 | Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
121 |
122 | If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement".
123 | Don't forget to give the project a star! Thanks again!
124 |
125 | 1. Fork the Project
126 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
127 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
128 | 4. Push to the Branch (`git push origin feature/AmazingFeature`)
129 | 5. Open a Pull Request
130 |
131 |
152 |
153 |
154 |
155 |
156 | [contributors-shield]: https://img.shields.io/github/contributors/oslabs-beta/ASTRO.svg?style=for-the-badge
157 | [contributors-url]: https://github.com/oslabs-beta/ASTRO/graphs/contributors
158 | [forks-shield]: https://img.shields.io/github/forks/oslabs-beta/ASTRO.svg?style=for-the-badge
159 | [forks-url]: https://github.com/oslabs-beta/ASTRO/network/members
160 | [stars-shield]: https://img.shields.io/github/stars/oslabs-beta/ASTRO.svg?style=for-the-badge
161 | [stars-url]: https://github.com/oslabs-beta/ASTRO/stargazers
162 | [issues-shield]: https://img.shields.io/github/issues/oslabs-beta/ASTRO.svg?style=for-the-badge
163 | [issues-url]: https://github.com/oslabs-beta/ASTRO/issues
164 | [license-shield]: https://img.shields.io/github/license/oslabs-beta/ASTRO.svg?style=for-the-badge
165 | [license-url]: https://github.com/oslabs-beta/ASTRO/blob/master/LICENSE.txt
166 | [linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555
167 | [linkedin-url]: https://linkedin.com/in/projectASTRO
168 | [product-screenshot]: public/astro-banner.jpeg
169 |
--------------------------------------------------------------------------------
/server/controllers/aws/Logs/updateLogs.js:
--------------------------------------------------------------------------------
1 | const moment = require('moment');
2 | const {
3 | CloudWatchLogsClient,
4 | FilterLogEventsCommand,
5 | DescribeLogStreamsCommand,
6 | } = require('@aws-sdk/client-cloudwatch-logs');
7 |
8 | const updateLogs = async (req, res, next) => {
9 | const oldFunctionLogs = req.body.logs;
10 | const functionsToFetch = [];
11 |
12 | // create an array with just the names of the functions that need to be refetched
13 | for (let i = 0; i < oldFunctionLogs.length; i += 1) {
14 | if (oldFunctionLogs[i].timePeriod !== req.body.newTimePeriod) {
15 | functionsToFetch.push(oldFunctionLogs[i].name);
16 | }
17 | }
18 |
19 | // StartTime and EndTime for CloudWatchLogsClient need to be in millisecond format so need to find what the provided time period equates to
20 | let StartTime;
21 | if (req.body.newTimePeriod === '30min') {
22 | StartTime = new Date(
23 | new Date().setMinutes(new Date().getMinutes() - 30)
24 | ).valueOf();
25 | } else if (req.body.newTimePeriod === '1hr') {
26 | StartTime = new Date(
27 | new Date().setMinutes(new Date().getMinutes() - 60)
28 | ).valueOf();
29 | } else if (req.body.newTimePeriod === '24hr') {
30 | StartTime = new Date(
31 | new Date().setDate(new Date().getDate() - 1)
32 | ).valueOf();
33 | } else if (req.body.newTimePeriod === '7d') {
34 | StartTime = new Date(
35 | new Date().setDate(new Date().getDate() - 7)
36 | ).valueOf();
37 | } else if (req.body.newTimePeriod === '14d') {
38 | StartTime = new Date(
39 | new Date().setDate(new Date().getDate() - 14)
40 | ).valueOf();
41 | } else if (req.body.newTimePeriod === '30d') {
42 | StartTime = new Date(
43 | new Date().setDate(new Date().getDate() - 30)
44 | ).valueOf();
45 | }
46 |
47 | const updatedArr = [];
48 | // loop through the function names and refetch logs for each of them using loopFunc
49 | for (let i = 0; i < functionsToFetch.length; i += 1) {
50 | const functionName = functionsToFetch[i];
51 | const newLogObj = await loopFunc(
52 | functionName,
53 | StartTime,
54 | req.body.credentials,
55 | req.body.newTimePeriod,
56 | req.body.region
57 | );
58 | // push individual log object onto updatedArr to be sent back to frontend
59 | updatedArr.push(newLogObj);
60 | }
61 | res.locals.updatedLogs = updatedArr;
62 | return next();
63 | };
64 |
65 | module.exports = updateLogs;
66 |
67 | // handles fetching for individual function
68 | const loopFunc = async (
69 | functionName,
70 | StartTime,
71 | credentials,
72 | timePeriod,
73 | region
74 | ) => {
75 | // create new CloudWatchLogsClient
76 | const cwLogsClient = new CloudWatchLogsClient({
77 | region,
78 | credentials: credentials,
79 | });
80 |
81 | // if a nextToken exists (meaning there are more logs to fetch), helperFunc provides a recursive way to get all the logs
82 | async function helperFunc(nextToken, data = []) {
83 | // once we run out of nextTokens, return data
84 | if (!nextToken) {
85 | return data;
86 | }
87 | const nextLogEvents = await cwLogsClient.send(
88 | new FilterLogEventsCommand({
89 | logGroupName: '/aws/lambda/' + functionName,
90 | endTime: new Date().valueOf(),
91 | startTime: StartTime,
92 | nextToken,
93 | filterPattern: '- START - END - REPORT',
94 | })
95 | );
96 | data.push(nextLogEvents.events);
97 | return helperFunc(nextLogEvents.nextToken, data);
98 | }
99 |
100 | try {
101 | // find the logEvents with given logGroupName and time period
102 | const logEvents = await cwLogsClient.send(
103 | new FilterLogEventsCommand({
104 | logGroupName: '/aws/lambda/' + functionName,
105 | endTime: new Date().valueOf(),
106 | startTime: StartTime,
107 | filterPattern: '- START - END - REPORT',
108 | })
109 | );
110 | // only send back most recent 50 logs to reduce size
111 | const shortenedEvents = [];
112 |
113 | // if we received a nextToken, start helperFunc process and make sure to parse through that data in order to grab from the end
114 | if (logEvents.nextToken) {
115 | const helperFuncResults = await helperFunc(logEvents.nextToken);
116 | let poppedEl;
117 | // while we still have logs to grab from the helperFunc and shortenedEvents is shorter than 50 logs, add to it from the end (giving us the most recent first instead)
118 | while (helperFuncResults.length) {
119 | poppedEl = helperFuncResults.pop();
120 | for (let i = poppedEl.length - 1; i >= 0; i -= 1) {
121 | if (shortenedEvents.length === 50) {
122 | break;
123 | }
124 | shortenedEvents.push(poppedEl[i]);
125 | }
126 | }
127 | }
128 |
129 | // if we didn't have a nextToken and got all logs in one request to the CloudWatchLogsClient
130 | if (!logEvents.nextToken) {
131 | // grab from the end to grab most recent logs and stop once we reach 50 to send back to frontend
132 | for (let i = logEvents.events.length - 1; i >= 0; i -= 1) {
133 | if (shortenedEvents.length === 50) break;
134 | shortenedEvents.push(logEvents.events[i]);
135 | }
136 | }
137 |
138 | // start forming what it'll look like to send back to frontend
139 | const eventLog = {
140 | name: functionName,
141 | timePeriod,
142 | };
143 | const streams = [];
144 |
145 | // loop through logs in order to eventually add to eventLog object
146 | for (let i = 0; i < shortenedEvents.length; i += 1) {
147 | let eventObj = shortenedEvents[i];
148 | // create the individual arrays to populate the table, this info makes up one row
149 | const dataArr = [];
150 | // just cut off the last five characters for the log stream name as an identifier
151 | dataArr.push('...' + eventObj.logStreamName.slice(-5));
152 | // format the date of the log timestamp to be more readable
153 | dataArr.push(moment(eventObj.timestamp).format('lll'));
154 | // if message is just from a normal log, remove the first 67 characters as it's all just metadata/a string of timestamps and unnecessary info
155 | if (
156 | eventObj.message.slice(0, 4) !== 'LOGS' &&
157 | eventObj.message.slice(0, 9) !== 'EXTENSION'
158 | ) {
159 | dataArr.push(eventObj.message.slice(67));
160 | // if the message starts with LOGS or EXTENSION, it's usually different type of info and the beginning part has to stay
161 | } else {
162 | dataArr.push(eventObj.message);
163 | }
164 | // push to the larger array to then make up the table
165 | streams.push(dataArr);
166 | }
167 | eventLog.streams = streams;
168 |
169 | // grab just the ERROR logs
170 | try {
171 | const errorEvents = await cwLogsClient.send(
172 | new FilterLogEventsCommand({
173 | logGroupName: '/aws/lambda/' + functionName,
174 | endTime: new Date().valueOf(),
175 | startTime: StartTime,
176 | filterPattern: 'ERROR',
177 | })
178 | );
179 | const errorStreams = [];
180 | // grab from the end to sort the most recent first
181 | for (let i = errorEvents.events.length - 1; i >= 0; i -= 1) {
182 | let errorObj = errorEvents.events[i];
183 | const rowArr = [];
184 | // just cut off the last five characters for the log stream name as an identifier
185 | rowArr.push('...' + errorObj.logStreamName.slice(-5));
186 | // format the date of the log timestamp to be more readable
187 | rowArr.push(moment(errorObj.timestamp).format('lll'));
188 | // remove the first 67 characters as it's all just metadata/a string of timestamps and unnecessary info
189 | rowArr.push(errorObj.message.slice(67));
190 | errorStreams.push(rowArr);
191 | }
192 | eventLog.errors = errorStreams;
193 | // return eventLog object to then be pushed to the array that's sent back to frontend with updated logs
194 | return eventLog;
195 | } catch (err) {
196 | if (err) {
197 | console.error(err);
198 | }
199 | }
200 | } catch (err) {
201 | console.error(err);
202 | }
203 | };
204 |
--------------------------------------------------------------------------------
/src/pages/Navigation.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { useSelector, useDispatch } from 'react-redux';
3 | import { AccountTotals } from '../components/AccountTotals.jsx';
4 | import { Dashboard } from '../components/Dashboard.jsx';
5 |
6 | import { toggleChange } from '../features/slices/insightsToggleSlice';
7 | import { nameChange } from '../features/slices/chartSlice';
8 | import { getFuncs } from '../features/slices/funcListSlice';
9 |
10 | ////////////////////////////////////
11 | ///////// MUI STYLING //////////////
12 | ////////////////////////////////////
13 |
14 | import { styled, useTheme } from '@mui/material/styles';
15 | import Box from '@mui/material/Box';
16 | import MuiDrawer from '@mui/material/Drawer';
17 | import MuiAppBar from '@mui/material/AppBar';
18 | import Toolbar from '@mui/material/Toolbar';
19 | import List from '@mui/material/List';
20 | import CssBaseline from '@mui/material/CssBaseline';
21 | import Divider from '@mui/material/Divider';
22 | import IconButton from '@mui/material/IconButton';
23 | import MenuIcon from '@mui/icons-material/Menu';
24 | import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
25 | import ChevronRightIcon from '@mui/icons-material/ChevronRight';
26 | import ListItemButton from '@mui/material/ListItemButton';
27 | import ListItemIcon from '@mui/material/ListItemIcon';
28 | import ListItemText from '@mui/material/ListItemText';
29 | import Button from '@mui/material/Button';
30 | import Collapse from '@mui/material/Collapse';
31 | import ExpandLess from '@mui/icons-material/ExpandLess';
32 | import ExpandMore from '@mui/icons-material/ExpandMore';
33 | import FunctionsTwoToneIcon from '@mui/icons-material/FunctionsTwoTone';
34 | import AddBoxTwoToneIcon from '@mui/icons-material/AddBoxTwoTone';
35 |
36 | const drawerWidth = 240;
37 |
38 | const openedMixin = (theme) => ({
39 | width: drawerWidth,
40 | transition: theme.transitions.create('width', {
41 | easing: theme.transitions.easing.sharp,
42 | duration: theme.transitions.duration.enteringScreen,
43 | }),
44 | overflowX: 'hidden',
45 | });
46 |
47 | const closedMixin = (theme) => ({
48 | transition: theme.transitions.create('width', {
49 | easing: theme.transitions.easing.sharp,
50 | duration: theme.transitions.duration.leavingScreen,
51 | }),
52 | overflowX: 'hidden',
53 | width: `calc(${theme.spacing(7)} + 1px)`,
54 | [theme.breakpoints.up('sm')]: {
55 | width: `calc(${theme.spacing(8)} + 1px)`,
56 | },
57 | });
58 |
59 | const DrawerHeader = styled('div')(({ theme }) => ({
60 | display: 'flex',
61 | alignItems: 'center',
62 | justifyContent: 'flex-end',
63 | padding: theme.spacing(0, 1),
64 | // necessary for content to be below app bar
65 | ...theme.mixins.toolbar,
66 | }));
67 |
68 | const AppBar = styled(MuiAppBar, {
69 | shouldForwardProp: (prop) => prop !== 'open',
70 | })(({ theme, open }) => ({
71 | zIndex: theme.zIndex.drawer + 1,
72 | transition: theme.transitions.create(['width', 'margin'], {
73 | easing: theme.transitions.easing.sharp,
74 | duration: theme.transitions.duration.leavingScreen,
75 | }),
76 | ...(open && {
77 | marginLeft: drawerWidth,
78 | width: `calc(100% - ${drawerWidth}px)`,
79 | transition: theme.transitions.create(['width', 'margin'], {
80 | easing: theme.transitions.easing.sharp,
81 | duration: theme.transitions.duration.enteringScreen,
82 | }),
83 | }),
84 | }));
85 |
86 | const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== 'open' })(
87 | ({ theme, open }) => ({
88 | width: drawerWidth,
89 | flexShrink: 0,
90 | whiteSpace: 'nowrap',
91 | boxSizing: 'border-box',
92 | ...(open && {
93 | ...openedMixin(theme),
94 | '& .MuiDrawer-paper': openedMixin(theme),
95 | }),
96 | ...(!open && {
97 | ...closedMixin(theme),
98 | '& .MuiDrawer-paper': closedMixin(theme),
99 | }),
100 | }),
101 | );
102 |
103 | ////////////////////////////////////////
104 | ///////// MUI STYLING END //////////////
105 | ////////////////////////////////////////
106 |
107 |
108 | export const Navigation = () => {
109 |
110 | const dispatch = useDispatch();
111 | const theme = useTheme();
112 |
113 | const componentChange = useSelector((state) => state.toggleInsights.toggle);
114 | const list = useSelector((state) => state.funcList.funcList);
115 | const creds = useSelector((state) => state.creds);
116 |
117 | useEffect(() => {
118 | dispatch(getFuncs(creds))
119 | }, [])
120 |
121 | const [open, setOpen] = React.useState(false);
122 | const [dropDown, setDropDown] = React.useState(false);
123 |
124 | const handleDrawerOpen = () => {
125 | setOpen(true);
126 | };
127 |
128 | const handleDrawerClose = () => {
129 | setOpen(false);
130 | };
131 |
132 | const handleFunctionToggle = (key) => {
133 | dispatch(nameChange(key))
134 | };
135 |
136 | const handleDropDownComponentChange = (tab) => {
137 | dispatch(toggleChange(tab))
138 | setDropDown(!dropDown);
139 | };
140 |
141 | const handleComponentChange = (tab) => {
142 | dispatch(toggleChange(tab))
143 | };
144 |
145 |
146 | const componentSwitch = (componentName) => {
147 | switch(componentName){
148 | case 'Account Totals':
149 | return
150 | case 'Functions':
151 | return
152 | }
153 | }
154 |
155 |
156 | return (
157 |
158 |
159 |
160 | {/* NAVIGATION HEADER */}
161 |
162 |
163 |
164 |
174 |
175 |
176 |
177 |
184 |
185 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 | {theme.direction === 'rtl' ? : }
200 |
201 |
202 |
203 |
204 |
205 | {/* NAVIGATION SIDEBAR */}
206 |
207 |
208 |
209 | handleComponentChange("Account Totals")}
211 | >
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 | {handleDropDownComponentChange("Functions")}}
221 | >
222 |
223 |
224 |
225 |
226 | {dropDown ? : }
227 |
228 |
229 |
230 |
231 |
232 | {list.map((text, index) => (
233 | handleFunctionToggle(index)}>
234 |
235 |
236 | ))}
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 | {/* COMPONENT RENDERING */}
247 |
248 |
249 |
250 |
251 |
252 | {componentSwitch(componentChange)}
253 |
254 |
255 |
256 |
257 | );
258 | }
259 |
--------------------------------------------------------------------------------
/src/components/AccountTotals.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { useSelector } from 'react-redux';
3 | import { metricsAllFunc } from '../utils/getMetricsAllFunc';
4 |
5 | ///STYLING - MATERIAL UI && CHART.JS///
6 | import Alert from '@mui/material/Alert';
7 | import AlertTitle from '@mui/material/AlertTitle';
8 | import Stack from '@mui/material/Stack';
9 | import Box from '@mui/material/Box';
10 | import Container from '@mui/material/Container';
11 | import Typography from '@mui/material/Typography';
12 | import Card from '@mui/material/Card';
13 | import CardContent from '@mui/material/CardContent';
14 | import { CardActionArea } from '@mui/material';
15 | import Paper from '@mui/material/Paper';
16 | import CircularProgress from '@mui/material/CircularProgress';
17 | import { Doughnut } from "react-chartjs-2";
18 | import 'chart.js/auto';
19 |
20 |
21 | export const AccountTotals = () => {
22 |
23 | const creds = useSelector((state) => state.creds)
24 | const chartData = useSelector((state) => state.data);
25 | const list = useSelector((state) => state.funcList.funcList);
26 |
27 | const [totalInvocations, setInvocations] = useState(0);
28 | const [totalThrottles, setThrottles] = useState(0);
29 | const [totalErrors, setErrors] = useState(0);
30 | const [pieChartInvocations, setPCI] = useState([]);
31 | const [pieChartErrors, setPCE] = useState([]);
32 | const [pieChartThrottles, setPCT] = useState([])
33 |
34 | /*
35 | Helper function that is called on load - retrieves the data needed to sum metric totals and store it in local state
36 | */
37 | const promise = (metric, setter) => {
38 | Promise.resolve(metricsAllFunc(creds, metric))
39 | .then(data => data.data.reduce((x, y) => x + y.y, 0))
40 | .then(data => setter(data))
41 | .catch(e => console.log(e))
42 | }
43 |
44 | /*
45 | Helper function to create customized formatted chart.js data based on function metric
46 | */
47 | const pieChartData = (funcNames, metric) =>{
48 | return {
49 | labels: [...funcNames],
50 | datasets: [
51 | {
52 | data: metric,
53 | backgroundColor: [
54 | "#64b5f6",
55 | "#9575cd",
56 | "#26a69a",
57 | "rgb(122,231,125)",
58 | "rgb(195,233,151)"
59 | ],
60 | hoverBackgroundColor: ["#1565c0", "#6200ea", "#004d40"]
61 | }
62 | ],
63 |
64 | plugins: {
65 | labels: {
66 | render: "percentage",
67 | fontColor: ["green", "white", "red"],
68 | precision: 2
69 | },
70 | },
71 | text: "23%",
72 | };
73 | }
74 |
75 | useEffect(() => {
76 |
77 | if (creds.region.length) {
78 | promise('Invocations', setInvocations);
79 | promise('Throttles', setThrottles);
80 | promise('Errors', setErrors);
81 | }
82 | if (chartData.data.invocations && chartData.data.errors && chartData.data.throttles) {
83 | const chartInvocations = [];
84 | for (let i = 0; i < chartData.data.invocations.length; i++) {
85 | chartInvocations.push(chartData.data.invocations[i].total);
86 | }
87 | setPCI(chartInvocations);
88 |
89 | const chartErrors = [];
90 | for (let i = 0; i < chartData.data.errors.length; i++) {
91 | chartErrors.push(chartData.data.errors[i].total);
92 | }
93 | setPCE(chartErrors);
94 |
95 | const chartThrottles = [];
96 | for (let i = 0; i < chartData.data.throttles.length; i++) {
97 | chartThrottles.push(chartData.data.throttles[i].total);
98 | }
99 | setPCT(chartThrottles);
100 |
101 | }
102 | } , [creds, chartData])
103 |
104 |
105 |
106 | return (
107 |
108 | chartData ?
109 |
110 |
113 |
114 |
122 |
Account Totals
123 |
124 |
125 |
126 |
127 |
135 |
136 |
137 |
138 |
146 |
147 |
148 |
149 |
150 |
151 |
152 | {/* INVOCATIONS CARD */}
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 | Invocations
161 | {totalInvocations}
162 |
163 |
164 |
183 |
184 |
185 |
186 |
187 |
188 |
189 | Invocations are the number of times a function was invoked by
190 | either an API call or an event response from another AWS
191 | service.
192 |
193 |
194 |
195 |
196 |
197 | {/* ERRORS CARD */}
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 | Errors
206 | {totalErrors}
207 |
208 |
209 |
228 |
229 |
230 |
231 |
232 |
233 | Errors log the number of errors thrown by a function. It can
234 | be used with the Invocations metric to calculate the total
235 | percentage of errors.
236 |
237 |
238 |
239 |
240 |
241 |
242 | {/* THROTTLES CARD */}
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 | Throttles
251 | {totalThrottles}
252 |
253 |
254 |
273 |
274 |
275 |
276 |
277 |
278 | Throttles occur when the number of invocations for a function
279 | exceeds its concurrency pool, which causes Lambda to start
280 | rejecting incoming requests.
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 | :
291 |
292 |
293 |
294 | );
295 | }
296 |
--------------------------------------------------------------------------------
/server/controllers/aws/Logs/getLogs.js:
--------------------------------------------------------------------------------
1 | const moment = require('moment');
2 |
3 | const {
4 | CloudWatchLogsClient,
5 | FilterLogEventsCommand,
6 | DescribeLogStreamsCommand,
7 | } = require('@aws-sdk/client-cloudwatch-logs');
8 |
9 | const getLogs = async (req, res, next) => {
10 | // append name of function to the format necessary for grabbing logs
11 | const logGroupName = '/aws/lambda/' + req.body.function;
12 |
13 | // start a new CloudWatchLogsClient connection with provided region and credentials
14 | const cwLogsClient = new CloudWatchLogsClient({
15 | region: req.body.region,
16 | credentials: req.body.credentials,
17 | });
18 |
19 | // StartTime and EndTime for CloudWatchLogsClient need to be in millisecond format so need to find what the provided time period equates to
20 | let StartTime;
21 | if (req.body.timePeriod === '30min') {
22 | StartTime = new Date(
23 | new Date().setMinutes(new Date().getMinutes() - 30)
24 | ).valueOf();
25 | } else if (req.body.timePeriod === '1hr') {
26 | StartTime = new Date(
27 | new Date().setMinutes(new Date().getMinutes() - 60)
28 | ).valueOf();
29 | } else if (req.body.timePeriod === '24hr') {
30 | StartTime = new Date(
31 | new Date().setDate(new Date().getDate() - 1)
32 | ).valueOf();
33 | } else if (req.body.timePeriod === '7d') {
34 | StartTime = new Date(
35 | new Date().setDate(new Date().getDate() - 7)
36 | ).valueOf();
37 | } else if (req.body.timePeriod === '14d') {
38 | StartTime = new Date(
39 | new Date().setDate(new Date().getDate() - 14)
40 | ).valueOf();
41 | } else if (req.body.timePeriod === '30d') {
42 | StartTime = new Date(
43 | new Date().setDate(new Date().getDate() - 30)
44 | ).valueOf();
45 | }
46 |
47 | // nextToken is a parameter specified by AWS CloudWatch for the FilterLogEventsCommand; this token is needed to fetch the next set of events
48 | // helperFunc provides a recursive way to get all the logs
49 | async function helperFunc(nextToken, data = []) {
50 | // once we run out of nextTokens, return data (base case)
51 | if (!nextToken) {
52 | return data;
53 | }
54 | const nextLogEvents = await cwLogsClient.send(
55 | // FilterLogEventsCommand is a class that lists log events from a specified log group, which can be filtered using a filter pattern, a time range, and/or the name of the log stream
56 | // by default this lists logs up to 1 megabyte of log events (~10,000 log events) but we are limiting the data to the most recent 50 log events
57 | // query will return results from LEAST recent to MOST recent
58 | new FilterLogEventsCommand({
59 | logGroupName,
60 | endTime: new Date().valueOf(),
61 | startTime: StartTime,
62 | nextToken,
63 | // START, END, REPORT are keywords that appear at the start of the message for specific log events and our filter pattern detects only these events to be included in our logs
64 | filterPattern: '- START - END - REPORT',
65 | })
66 | );
67 | data.push(nextLogEvents.events);
68 | return helperFunc(nextLogEvents.nextToken, data);
69 | }
70 |
71 | try {
72 | // find the logEvents with given logGroupName and time period
73 | const logEvents = await cwLogsClient.send(
74 | new FilterLogEventsCommand({
75 | logGroupName,
76 | endTime: new Date().valueOf(),
77 | startTime: StartTime,
78 | filterPattern: '- START - END - REPORT',
79 | })
80 | );
81 |
82 | // if no log events exist, return back to frontend
83 | if (!logEvents) {
84 | res.locals.functionLogs = false;
85 | return next();
86 | }
87 | // only send back most recent 50 logs to reduce size of payload
88 | const shortenedEvents = [];
89 |
90 | // if we received a nextToken, start helperFunc to recursively parse through most recent data (meaning we grab data from the end since that is the most recent log stream)
91 | if (logEvents.nextToken) {
92 | const helperFuncResults = await helperFunc(logEvents.nextToken);
93 |
94 | // poppedEl gets the most recent log stream that currently exists in helperFunc (log streams that are even more recent will have already been added to shortenedEvents)
95 | let poppedEl;
96 |
97 | // while we still have logs to grab from the helperFunc and shortenedEvents is shorter than 50 logs, add to shortenedEvents array from the end (the most recent log stream)
98 | while (
99 | helperFuncResults.length &&
100 | shortenedEvents.length <= 50
101 | ) {
102 | // poppedEl gets the most recent log stream that currently exists in helperFunc (log streams that are even more recent will have already been added to shortenedEvents)
103 | // but the for loop below is iterating through helperFunc such that we are adding the most recent log stream at the beginning of the shortenedEvent array
104 | poppedEl = helperFuncResults.pop();
105 | /**
106 |
107 | shortenedEvent = [ helperFuncResults = [
108 | index 0: { most recent event log stream }, index 0: { least recent event log stream }
109 | . .
110 | . .
111 | . .
112 | index N: { least recent event log stream }, index N: { most recent event log stream }
113 | ] ]
114 |
115 | */
116 | for (let i = poppedEl.length - 1; i >= 0; i -= 1) {
117 | // we don't want to have more than 50 logs at any point in time to reduce operational load and size
118 | if (shortenedEvents.length === 50) break;
119 | else shortenedEvents.push(poppedEl[i]);
120 | }
121 | }
122 | }
123 | /**
124 | * If a nextToken exists, we can't populate shortenedEvents with event log data without the second part of
125 | * the or clause since we want to consider the situation when there are < 50 event log streams;
126 | */
127 | if (!logEvents.nextToken || shortenedEvents.length < 50) {
128 | // grab from the end to grab most recent logs and stop once we reach 50 to send back to frontend
129 | for (let i = logEvents.events.length - 1; i >= 0; i -= 1) {
130 | if (shortenedEvents.length === 50) break;
131 | shortenedEvents.push(logEvents.events[i]);
132 | }
133 | }
134 |
135 | // start forming what it'll look like to send back to frontend
136 | const eventLog = {
137 | name: req.body.function,
138 | timePeriod: req.body.timePeriod,
139 | };
140 |
141 | const streams = [];
142 |
143 | // loop through logs in order to eventually add to eventLog object
144 | for (let i = 0; i < shortenedEvents.length; i += 1) {
145 | // the very first shortenedEvent element is the most recent log stream
146 | let eventObj = shortenedEvents[i];
147 | // create the individual arrays to populate the table; note that this will represent a single row of info (log stream name + time stamp + stream message)
148 | const dataArr = [];
149 | // cut off the last five characters from the log stream name to create an identifier for this specific log stream
150 | // note that logStreamName appears before the timestamp
151 | dataArr.push('...' + eventObj.logStreamName.slice(-5));
152 | // format('lll') creates a human readable date from the specific log stream's timestamp
153 | dataArr.push(moment(eventObj.timestamp).format('lll'));
154 |
155 | // if message is just from a normal log, remove the first 67 characters of the message as it's all just metadata/a string of timestamps and unnecessary info
156 | if (
157 | eventObj.message.slice(0, 4) !== 'LOGS' &&
158 | eventObj.message.slice(0, 9) !== 'EXTENSION'
159 | )
160 | dataArr.push(eventObj.message.slice(67));
161 | // messages starting with LOGS or EXTENSION represents different/pertinent info and we don't want to mutate the message like we did within the if block just above
162 | else dataArr.push(eventObj.message);
163 | // push the formatted dataArr into the outer array, streams, to make the table for our logs
164 | streams.push(dataArr);
165 | }
166 | eventLog.streams = streams;
167 | /**
168 |
169 | streams = [
170 | index 0: [ { most recent event log stream } ],
171 | .
172 | .
173 | .
174 | index N: [ { least recent event log stream } ],
175 | ]
176 | */
177 |
178 | // grab just the ERROR logs
179 | try {
180 | const errorEvents = await cwLogsClient.send(
181 | new FilterLogEventsCommand({
182 | logGroupName,
183 | endTime: new Date().valueOf(),
184 | startTime: StartTime,
185 | filterPattern: 'ERROR',
186 | })
187 | );
188 | const errorStreams = [];
189 | // grab from the end to sort the most recent first
190 | for (let i = errorEvents.events.length - 1; i >= 0; i -= 1) {
191 | let errorObj = errorEvents.events[i];
192 | const rowArr = [];
193 | // just cut off the last five characters for the log stream name as an identifier
194 | rowArr.push('...' + errorObj.logStreamName.slice(-5));
195 | // format the date of the log timestamp to be more readable
196 | rowArr.push(moment(errorObj.timestamp).format('lll'));
197 | // remove the first 67 characters as it's all just metadata/a string of timestamps and unnecessary info
198 | rowArr.push(errorObj.message.slice(67));
199 | errorStreams.push(rowArr);
200 | }
201 | eventLog.errors = errorStreams;
202 | // send entire object back to frontend
203 | res.locals.functionLogs = eventLog;
204 | return next();
205 | } catch (err) {
206 | if (err) {
207 | console.error(err);
208 |
209 | return next(err);
210 | }
211 | }
212 | } catch (err) {
213 | if (err) console.error(err);
214 | return next(err);
215 | }
216 | };
217 |
218 | module.exports = getLogs;
219 |
--------------------------------------------------------------------------------