├── .eslintrc.json ├── .gitignore ├── README.md ├── __tests__ └── supertest.js ├── assets └── images │ ├── Alerts-Demo.gif │ ├── Example-Email.png │ ├── Hermes-A-Gradient-cropped.png │ ├── Index-Patterns-Demo.gif │ ├── View-Logs-Demo.gif │ └── Visualize-Logs-Demo.gif ├── client ├── assets │ ├── Hermes-A-Gold.png │ └── Hermes-A.png ├── atom.jsx ├── components │ ├── AlertSearchBox.jsx │ ├── Alerts.jsx │ ├── CreateAlert.jsx │ ├── EditorContainer.jsx │ ├── HomePage.jsx │ ├── Indices.jsx │ ├── Logs.jsx │ ├── MonitorButton.jsx │ ├── Nav.jsx │ ├── Row.jsx │ ├── SelectBox.jsx │ ├── SideBar.jsx │ ├── SimpleTable.jsx │ └── UserLineChart.jsx ├── containers │ ├── AlertsContainer.jsx │ ├── App.jsx │ ├── CreateIndex.jsx │ ├── LogsContainer.jsx │ └── Visualizer.jsx ├── hooks │ └── useAxios.jsx ├── index.jsx ├── monitor-funcs │ └── monitorFunc.js └── styles │ ├── _alerts.scss │ ├── _base.scss │ ├── _createindex.scss │ ├── _layout.scss │ ├── _logs.scss │ ├── _navigation.scss │ ├── _sidebar.scss │ ├── _visualizer.scss │ ├── components │ ├── _button.scss │ └── _card.scss │ └── main.scss ├── email_smtp.js ├── index.html ├── package.json ├── server ├── controllers │ ├── alertsController.js │ ├── indexPatternsController.js │ └── logsController.js ├── routes │ ├── alertsRouter.js │ ├── indexPatternsRouter.js │ └── logsRouter.js └── server.js ├── storage ├── alerts.json └── index_patterns.json └── webpack.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true, 6 | "jest": true 7 | }, 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:react/recommended" 11 | ], 12 | "parserOptions": { 13 | "ecmaFeatures": { 14 | "jsx": true 15 | }, 16 | "ecmaVersion": 12, 17 | "sourceType": "module" 18 | }, 19 | "plugins": [ 20 | "react" 21 | ], 22 | "rules": { 23 | "indent": [ 24 | "error", 25 | 2 26 | ], 27 | "linebreak-style": [ 28 | "error", 29 | "unix" 30 | ], 31 | "semi": [ 32 | "error" 33 | ] 34 | } 35 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | .DS_Store 4 | build/ 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

6 | A customizable alert manager for Elasticsearch 7 |

8 | 9 | --- 10 | 11 | ## Alerts 12 | 13 | View and delete existing alert configurations that are continually monitored at your desired frequency. Create alerts by specifying the new alert name, the index pattern to monitor, the frequency at which the Elasticsearch cluster will be searched, the rule to search for in the cluster, and the customized email that will be sent when the Elasticsearch query responds with a hit. Hermes uses Mustache.js which allows you to include any field from the top hit in the body of your alert email. 14 | 15 |

16 | 17 |

18 | 19 | ## Example Email 20 | 21 |

22 | 23 |

24 | 25 | ## View Logs 26 | 27 | View individual logs using a simple filter. 28 | 29 |

30 | 31 |

32 | 33 | ## Visualize Logs 34 | 35 | Visualize the count of logs that were created every hour over the past two weeks. 36 | 37 |

38 | 39 |

40 | 41 | ## Index Patterns 42 | 43 | Create and delete index patterns so that you can query multiple indices from your Elasticsearch cluster at once. 44 | 45 |

46 | 47 |

48 | 49 | # Getting started 50 | 51 | ## Deploying Hermes 52 | 53 | Hermes requires your Elasticsearch cluster to already be set up, running, and accessible on port 9200. 54 | 55 | Running Hermes the first time: 56 | 57 | 1. Fork and clone the main branch of this repository 58 | 2. Make sure you are in the outer-most directory of the repository in your command line 59 | 3. Configure the email account that will send alert messages per the instructions in [Configuring SMTP With Gmail](#configuring-smtp-with-gmail) 60 | 4. Run `npm install` 61 | 5. Run `npm run build` 62 | 6. Run `npm start` 63 | 7. Open your web browser to http://localhost:3068 64 | 65 | --- 66 | 67 | ## Configuring SMTP With Gmail 68 | 69 | 1. Create a new Gmail account. 70 | 2. Open settings and disable Two-Factor Authentication and allow access in Gmail for less secure apps. Configuring these settings will allow Hermes to send emails on your behalf. 71 | 3. Open the `email_smtp.js` file located in the top directory of your cloned repository. 72 | 4. Enter your new password into `Password` and your new email address into the `Username` and `From` properties of the object passed into `Email.send()` 73 | 5. Save your changes. 74 | 75 | --- 76 | 77 | ## Contributors 78 | 79 | Eric Olaya
80 | [LinkedIn](https://www.linkedin.com/in/eric-olaya/) | [Github](https://github.com/eric-olaya) 81 | 82 | Jared Lewis
83 | [LinkedIn](https://www.linkedin.com/in/jareddlewis/) | [Github](https://github.com/jaredDlewis/) 84 | 85 | Sheldon Johnson
86 | [LinkedIn](https://www.linkedin.com/in/sheldon-johnson-18a512106/) | [Github](https://github.com/avatarwnd) 87 | -------------------------------------------------------------------------------- /__tests__/supertest.js: -------------------------------------------------------------------------------- 1 | 2 | // const { response } = require('express'); 3 | const request = require('supertest'); 4 | 5 | const server = 'http://localhost:3068'; 6 | 7 | describe('Route integration', () => { 8 | describe('/', () => { 9 | describe('GET', () => { 10 | it('responds with 200 status and text/html content type', () => 11 | request(server) 12 | .get('/') 13 | .expect('Content-Type', /text\/html/) 14 | .expect(200)); 15 | }); 16 | }); 17 | describe('/logs/logsbyindex', () => { 18 | describe('GET', () => { 19 | it('responds with 200 status and application/json content type', () => 20 | request(server) 21 | .get('/logs/logsbyindex') 22 | .expect('Content-Type', /application\/json/) 23 | .expect(200)); 24 | }); 25 | }); 26 | describe('/logs/esindices', () => { 27 | describe('GET', () => { 28 | it('responds with 200 status and application/json content type', () => 29 | request(server) 30 | .get('/logs/esindices') 31 | .expect('Content-Type', /application\/json/) 32 | .expect(200)); 33 | }); 34 | }); 35 | describe('/logs/hourbuckets', () => { 36 | describe('GET', () => { 37 | it('responds with 200 status and text/html content type', () => 38 | request(server) 39 | .get('/logs/hourbuckets') 40 | .expect('Content-Type', /text\/html/)); 41 | 42 | }); 43 | }); 44 | describe('/logs/monitor', () => { 45 | describe('GET', () => { 46 | it('responds with 200 status and text/html content type', () => 47 | request(server) 48 | .get('/logs/monitor') 49 | .expect('Content-Type', /text\/html/)); 50 | 51 | }); 52 | }); 53 | describe('/indexpatterns', () => { 54 | describe('GET', () => { 55 | it('responds with 200 status and application/json content type', () => 56 | request(server) 57 | .get('/indexpatterns') 58 | .expect('Content-Type', /application\/json/) 59 | .expect(200)); 60 | }); 61 | }); 62 | describe('/alerts', () => { 63 | describe('GET', () => { 64 | it('responds with 200 status and application/json content type', () => 65 | request(server) 66 | .get('/alerts') 67 | .expect('Content-Type', /application\/json/) 68 | .expect(200)); 69 | }); 70 | }); 71 | }); -------------------------------------------------------------------------------- /assets/images/Alerts-Demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Hermes/41baf47a55a63bd9a8af882f3767fbb63cbce49e/assets/images/Alerts-Demo.gif -------------------------------------------------------------------------------- /assets/images/Example-Email.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Hermes/41baf47a55a63bd9a8af882f3767fbb63cbce49e/assets/images/Example-Email.png -------------------------------------------------------------------------------- /assets/images/Hermes-A-Gradient-cropped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Hermes/41baf47a55a63bd9a8af882f3767fbb63cbce49e/assets/images/Hermes-A-Gradient-cropped.png -------------------------------------------------------------------------------- /assets/images/Index-Patterns-Demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Hermes/41baf47a55a63bd9a8af882f3767fbb63cbce49e/assets/images/Index-Patterns-Demo.gif -------------------------------------------------------------------------------- /assets/images/View-Logs-Demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Hermes/41baf47a55a63bd9a8af882f3767fbb63cbce49e/assets/images/View-Logs-Demo.gif -------------------------------------------------------------------------------- /assets/images/Visualize-Logs-Demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Hermes/41baf47a55a63bd9a8af882f3767fbb63cbce49e/assets/images/Visualize-Logs-Demo.gif -------------------------------------------------------------------------------- /client/assets/Hermes-A-Gold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Hermes/41baf47a55a63bd9a8af882f3767fbb63cbce49e/client/assets/Hermes-A-Gold.png -------------------------------------------------------------------------------- /client/assets/Hermes-A.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Hermes/41baf47a55a63bd9a8af882f3767fbb63cbce49e/client/assets/Hermes-A.png -------------------------------------------------------------------------------- /client/atom.jsx: -------------------------------------------------------------------------------- 1 | import { atom } from 'recoil'; 2 | 3 | export const alertsState = atom({ 4 | key: 'alertsState', 5 | default: [], 6 | }); 7 | 8 | export const logsState = atom({ 9 | key: 'logsState', 10 | default: [], 11 | }); 12 | 13 | export const intervalIdsState = atom({ 14 | key: 'intervalIdsState', 15 | default: [], 16 | }); 17 | 18 | export const monitorStatusState = atom({ 19 | key: 'monitorStatusState', 20 | default: 'Off', 21 | }); 22 | 23 | export const indexPatternsState = atom({ 24 | key: 'indexPatternsState', 25 | default: [], 26 | }); 27 | 28 | export const lastChosenIndexPatternState = atom({ 29 | key: 'lastChosenIndexPatternState', 30 | default: '', 31 | }); 32 | 33 | export const createAlertInputState = atom({ 34 | key: 'createAlertInputState', 35 | default: { 36 | alertName: '', 37 | monitorFrequency: '', 38 | monitorFrequencyUnit: '', 39 | emailAddress: '', 40 | emailSubject: '', 41 | emailBody: 42 | 'Within the last hour, there was at least one log with "ERROR" in it. The top hit contained the following log: {{log}}', 43 | editorContents: `{ 44 | "bool": { 45 | "must": [ 46 | { 47 | "match": { 48 | "log": "ERROR" 49 | } 50 | }, 51 | { 52 | "range": { 53 | "@timestamp": { 54 | "gte": "now-1h/h", 55 | "lte": "now/h" 56 | } 57 | } 58 | } 59 | ] 60 | } 61 | }`, 62 | }, 63 | }); 64 | 65 | export const currentAlertsState = atom({ 66 | key: 'currentAlertsState', 67 | default: [], 68 | }); 69 | 70 | export const alertSearchBoxState = atom({ 71 | key: 'alertSearchBoxState', 72 | default: '', 73 | }); 74 | 75 | export const monitorFrequencyInputState = atom({ 76 | key: 'monitorFrequencyInputState', 77 | default: '', 78 | }); 79 | 80 | export const monitorFrequencyUnitInputState = atom({ 81 | key: 'monitorFrequencyUnitInputState', 82 | default: '', 83 | }); 84 | -------------------------------------------------------------------------------- /client/components/AlertSearchBox.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TextField from '@mui/material/TextField'; 3 | import { alertSearchBoxState } from '../atom'; 4 | import { useRecoilState } from 'recoil'; 5 | 6 | export default function AlertSearchBox() { 7 | const [alertSearchBox, setAlertSearchBox] = 8 | useRecoilState(alertSearchBoxState); 9 | const handleChange = (event) => { 10 | const newAlertSearch = event.target.value; 11 | setAlertSearchBox(newAlertSearch); 12 | }; 13 | return ( 14 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /client/components/Alerts.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Box from '@mui/material/Box'; 4 | import Collapse from '@mui/material/Collapse'; 5 | import IconButton from '@mui/material/IconButton'; 6 | import Table from '@mui/material/Table'; 7 | import TableBody from '@mui/material/TableBody'; 8 | import TableCell from '@mui/material/TableCell'; 9 | import TableContainer from '@mui/material/TableContainer'; 10 | import TableHead from '@mui/material/TableHead'; 11 | import TableRow from '@mui/material/TableRow'; 12 | import Typography from '@mui/material/Typography'; 13 | import Paper from '@mui/material/Paper'; 14 | import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown'; 15 | import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp'; 16 | import VolumeUpIcon from '@material-ui/icons/VolumeUp'; 17 | import DeleteIcon from '@material-ui/icons/Delete'; 18 | import BlockIcon from '@material-ui/icons/Block'; 19 | import { useRecoilState, useRecoilValue } from 'recoil'; 20 | import { currentAlertsState, alertSearchBoxState } from '../atom'; 21 | import axios from 'axios'; 22 | 23 | function Row(props) { 24 | const { row } = props; 25 | const [open, setOpen] = React.useState(false); 26 | const [currentAlerts, setCurrentAlerts] = useRecoilState(currentAlertsState); 27 | const alertSearchBox = useRecoilValue(alertSearchBoxState); 28 | const frequencyConverter = (frequency, value) => { 29 | let adjustedFrequency = frequency; 30 | switch (value) { 31 | case 'day(s)': 32 | adjustedFrequency = adjustedFrequency / 86400 / 1000; 33 | break; 34 | case 'hour(s)': 35 | adjustedFrequency = adjustedFrequency / 3600 / 1000; 36 | break; 37 | case 'minute(s)': 38 | adjustedFrequency = adjustedFrequency / 60 / 1000; 39 | break; 40 | case 'second(s)': 41 | adjustedFrequency = adjustedFrequency / 1000; 42 | break; 43 | default: 44 | console.log('Error in frequencyConverter'); 45 | break; 46 | } 47 | return adjustedFrequency; 48 | }; 49 | const reducedMonitorFreq = frequencyConverter( 50 | row.monitorFrequency, 51 | row.monitorFrequencyUnit 52 | ); 53 | 54 | const deleteAlert = () => { 55 | axios 56 | .delete('/alerts', { data: { alert: row } }) 57 | .then((result) => { 58 | setCurrentAlerts(result.data); 59 | }) 60 | .catch((error) => 61 | console.log('Error in CreateAlert handleClickCreate: ', error) 62 | ); 63 | }; 64 | const regex = new RegExp(alertSearchBox, 'i'); 65 | if (alertSearchBox === '' || regex.test(row.alertName)) { 66 | return ( 67 | 68 | 69 | 70 | setOpen(!open)} 74 | > 75 | {open ? : } 76 | 77 | 78 | 79 | {row.alertName} 80 | 81 | 82 | {row.indexPattern} 83 | 84 | 85 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | Details 96 | 97 | 107 | 108 | 109 | Check Period 110 | 111 | {' '} 112 | {reducedMonitorFreq + ' ' + row.monitorFrequencyUnit} 113 | 114 | 115 | 116 | Email Address 117 | {row.emailAddress} 118 | 119 | 120 | Email Subject 121 | {row.emailSubject} 122 | 123 | 124 |
125 | 126 | Message 127 | 128 | 138 | 139 | 140 | {row.emailBody} 141 | 142 | 143 |
144 | 145 | Rule 146 | 147 | 157 | 158 | 159 | {row.editorContents} 160 | 161 | 162 |
163 |
164 |
165 |
166 |
167 |
168 | ); 169 | } 170 | return ; 171 | } 172 | 173 | Row.propTypes = { 174 | row: PropTypes.shape({ 175 | alertName: PropTypes.string.isRequired, 176 | monitorFrequency: PropTypes.number.isRequired, 177 | emailAddress: PropTypes.string.isRequired, 178 | emailSubject: PropTypes.string.isRequired, 179 | emailBody: PropTypes.string.isRequired, 180 | indexPattern: PropTypes.string.isRequired, 181 | monitorFrequencyUnit: PropTypes.string.isRequired, 182 | editorContents: PropTypes.string.isRequired, 183 | }).isRequired, 184 | }; 185 | 186 | export default function CollapsibleTable() { 187 | const [currentAlerts, setCurrentAlerts] = useRecoilState(currentAlertsState); 188 | const [alertSearchBox] = useRecoilState(alertSearchBoxState); 189 | useEffect(() => { 190 | axios 191 | .get('/alerts') 192 | .then((result) => setCurrentAlerts(result.data)) 193 | .catch((error) => console.log('Error in Alerts useEffect: ', error)); 194 | }, []); 195 | 196 | return ( 197 | 198 | 209 | 210 | 211 | 212 | Alert Name 213 | 214 | Index Pattern 215 | 216 | 217 | Delete 218 | 219 | 220 | 221 | 222 | {currentAlerts.map((alert) => ( 223 | 224 | ))} 225 | 226 |
227 |
228 | ); 229 | } 230 | -------------------------------------------------------------------------------- /client/components/CreateAlert.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import Button from '@mui/material/Button'; 3 | import TextField from '@mui/material/TextField'; 4 | import Dialog from '@mui/material/Dialog'; 5 | import DialogActions from '@mui/material/DialogActions'; 6 | import DialogContent from '@mui/material/DialogContent'; 7 | import DialogContentText from '@mui/material/DialogContentText'; 8 | import DialogTitle from '@mui/material/DialogTitle'; 9 | import EditorContainer from './EditorContainer'; 10 | import { Link } from '@mui/material'; 11 | import { 12 | indexPatternsState, 13 | createAlertInputState, 14 | currentAlertsState, 15 | lastChosenIndexPatternState, 16 | monitorFrequencyInputState, 17 | } from '../atom'; 18 | import { useRecoilState } from 'recoil'; 19 | import axios from 'axios'; 20 | import SelectBox from './SelectBox'; 21 | 22 | const defaultStateValues = { 23 | alertName: '', 24 | monitorFrequency: '', 25 | monitorFrequencyUnit: '', 26 | emailAddress: '', 27 | emailSubject: '', 28 | emailBody: 29 | 'Within the last hour, there was at least one log with "ERROR" in it. The top hit contained the following log: {{log}}', 30 | indexPattern: '', 31 | }; 32 | 33 | export default function FormDialog() { 34 | const [open, setOpen] = useState(false); 35 | const [disableButton, setDisableButton] = useState(true); 36 | const [indexPatterns, setIndexPatterns] = useRecoilState(indexPatternsState); 37 | const [createAlertInput, setCreateAlertInput] = useRecoilState( 38 | createAlertInputState 39 | ); 40 | const [currentAlerts, setCurrentAlerts] = useRecoilState(currentAlertsState); 41 | const [lastChosenIndexPattern, setLastChosenIndexPattern] = useRecoilState( 42 | lastChosenIndexPatternState 43 | ); 44 | const [monitorFrequency, setMonitorFrequency] = useRecoilState( 45 | monitorFrequencyInputState 46 | ); 47 | 48 | useEffect(() => { 49 | axios 50 | .get('/indexpatterns') 51 | .then((result) => setIndexPatterns(result.data)) 52 | .catch((error) => console.log('Error in CreateAlert useEffect: ', error)); 53 | }, []); 54 | useEffect(() => { 55 | let activateCreateButton = true; 56 | for (const key in createAlertInput) { 57 | if (createAlertInput[key] === '' && key !== 'indexPattern') 58 | activateCreateButton = false; 59 | } 60 | if (lastChosenIndexPattern === '') activateCreateButton = false; 61 | setDisableButton(!activateCreateButton); 62 | }, [createAlertInput, lastChosenIndexPattern]); 63 | 64 | const handleChange = (event) => { 65 | const newCreateAlertInput = { ...createAlertInput }; 66 | //the Editor component automatically sends the value instead of the event to the handleChange function 67 | if (typeof event === 'string') { 68 | newCreateAlertInput.editorContents = event; 69 | setCreateAlertInput(newCreateAlertInput); 70 | } else { 71 | newCreateAlertInput[event.target.id] = event.target.value; 72 | setCreateAlertInput(newCreateAlertInput); 73 | } 74 | }; 75 | 76 | const handleClickOpen = () => { 77 | setOpen(true); 78 | }; 79 | 80 | const handleClose = () => { 81 | setOpen(false); 82 | const newCreateAlertInput = Object.assign( 83 | { ...createAlertInput }, 84 | defaultStateValues 85 | ); 86 | setMonitorFrequency(''); 87 | setCreateAlertInput(newCreateAlertInput); 88 | }; 89 | 90 | // add new alert to the state that contains all user's alerts 91 | const handleClickCreate = () => { 92 | const newAlert = { 93 | ...createAlertInput, 94 | indexPattern: lastChosenIndexPattern, 95 | }; 96 | axios 97 | .post('/alerts', { alert: newAlert }) 98 | .then((result) => { 99 | setCurrentAlerts(result.data); 100 | setOpen(false); 101 | const newCreateAlertInput = Object.assign( 102 | { ...createAlertInput }, 103 | defaultStateValues 104 | ); 105 | setMonitorFrequency(''); 106 | setCreateAlertInput(newCreateAlertInput); 107 | }) 108 | .catch((error) => 109 | console.log('Error in CreateAlert handleClickCreate: ', error) 110 | ); 111 | }; 112 | 113 | // handle change func passed down to the index pattern select box 114 | const handleDropdownChange = (event) => { 115 | setLastChosenIndexPattern(event.target.value); 116 | }; 117 | // unit options for dropdowns 118 | const units = ['day(s)', 'hour(s)', 'minute(s)', 'second(s)']; 119 | // array from 1 to 60 120 | const numbers = Array.from({ length: 60 }, (_, i) => i + 1); 121 | 122 | // converts a frequency to milliseconds 123 | const frequencyConverter = (frequency, value) => { 124 | let adjustedFrequency = frequency; 125 | switch (value) { 126 | case 'day(s)': 127 | adjustedFrequency *= 86400 * 1000; 128 | break; 129 | case 'hour(s)': 130 | adjustedFrequency *= 3600 * 1000; 131 | break; 132 | case 'minute(s)': 133 | adjustedFrequency *= 60 * 1000; 134 | break; 135 | case 'second(s)': 136 | adjustedFrequency *= 1000; 137 | break; 138 | default: 139 | console.log('Error in frequencyConverter'); 140 | break; 141 | } 142 | return adjustedFrequency; 143 | }; 144 | 145 | const handleMonitorFrequencyChange = (event) => { 146 | setMonitorFrequency(event.target.value); 147 | }; 148 | const handleMonitorFrequencyUnitChange = (event) => { 149 | // convert frequency to milliseconds and add to state 150 | const convertedMonitorFrequency = frequencyConverter( 151 | monitorFrequency, 152 | event.target.value 153 | ); 154 | setCreateAlertInput({ 155 | ...createAlertInput, 156 | monitorFrequency: convertedMonitorFrequency, 157 | monitorFrequencyUnit: event.target.value, 158 | }); 159 | }; 160 | return ( 161 |
162 | 173 | 180 | Create Alert 181 | 182 | 183 | Configure your alert details below. 184 | 185 |
186 | 198 | 208 | 218 | 228 |
229 | 230 | Use the editor below to enter the customized rule for your alert. 231 | 232 |
233 | 237 |
238 |
239 | 252 | 264 | 280 | 296 |
297 | 298 | In the email body, you can use  299 | 303 | {{ mustache }} 304 | 305 |  template syntax to access any field from the log that had the 306 | highest score of your hits. 307 | 308 | 321 |
322 | 323 | 326 | 333 | 334 |
335 |
336 | ); 337 | } 338 | -------------------------------------------------------------------------------- /client/components/EditorContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Editor from '@monaco-editor/react'; 3 | 4 | const EditorContainer = ({ editorContents, handleChange }) => { 5 | return ( 6 | 20 | ); 21 | }; 22 | 23 | export default EditorContainer; 24 | -------------------------------------------------------------------------------- /client/components/HomePage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import VisibilityIcon from '@material-ui/icons/Visibility'; 4 | import TimelineIcon from '@material-ui/icons/Timeline'; 5 | import AnnouncementIcon from '@material-ui/icons/Announcement'; 6 | import CreateIcon from '@material-ui/icons/Create'; 7 | const HomePage = () => { 8 | return ( 9 | <> 10 |
11 | 12 |
13 |
14 |
 
15 |

16 | 17 | Alerts 18 | 19 |

20 |
21 | 22 |
23 |
24 |
    25 |
  • 26 | Create alerts and configure custom notification actions 27 |
  • 28 |
29 |
30 |
31 |
32 | 33 | 34 | 35 |
36 |
37 |
 
38 |

39 | 40 | View Logs 41 | 42 |

43 |
44 | 45 |
46 |
47 |
    48 |
  • View and filter log data from a cluster
  • 49 |
50 |
51 |
52 |
53 | 54 | 55 | 56 |
57 |
58 |
 
59 |

60 | 61 | Visualize Logs 62 | 63 |

64 |
65 | 66 |
67 |
68 |
    69 |
  • View recent log counts per hour
  • 70 |
71 |
72 |
73 |
74 | 75 | 76 | 77 |
78 |
79 |
 
80 |

81 | 82 | Index Patterns 83 | 84 |

85 |
86 | 87 |
88 |
89 |
    90 |
  • 91 | Create and delete index patterns for searching your 92 | Elasticsearch cluster 93 |
  • 94 |
95 |
96 |
97 |
98 | 99 |
100 | 101 | ); 102 | }; 103 | 104 | export default HomePage; 105 | -------------------------------------------------------------------------------- /client/components/Indices.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function Indices({ name }) { 4 | return ( 5 |
6 |

{name}

7 |
8 | ); 9 | } 10 | 11 | export default Indices; 12 | -------------------------------------------------------------------------------- /client/components/Logs.jsx: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | 3 | // // eslint-disable-next-line react/prop-types 4 | // function Logs({ time, index }) { 5 | // return ( 6 | //
7 | //

8 | //
9 | // ); 10 | // } 11 | 12 | // export default Logs; 13 | -------------------------------------------------------------------------------- /client/components/MonitorButton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRecoilState, useRecoilValue } from 'recoil'; 3 | import { 4 | intervalIdsState, 5 | monitorStatusState, 6 | currentAlertsState, 7 | } from '../atom'; 8 | import Button from '@mui/material/Button'; 9 | import monitorFunc from '../monitor-funcs/monitorFunc'; 10 | 11 | const MonitorButton = ({ functional }) => { 12 | const [monitorStatus, setMonitorStatus] = useRecoilState(monitorStatusState); 13 | const [intervalIds, setIntervalIds] = useRecoilState(intervalIdsState); 14 | const currentAlerts = useRecoilValue(currentAlertsState); 15 | const clickHandler = () => { 16 | // iterate through the currentAlerts array and start an interval for each alert object 17 | if (monitorStatus === 'Off') { 18 | const newIntervalIds = []; 19 | for (const alert of currentAlerts) { 20 | newIntervalIds.push(monitorFunc(alert)); 21 | } 22 | setIntervalIds(newIntervalIds); 23 | setMonitorStatus('On'); 24 | } else { 25 | // iterate through timerIds array and clear each interval 26 | for (const intervalId of intervalIds) { 27 | clearInterval(intervalId); 28 | } 29 | setMonitorStatus('Off'); 30 | } 31 | }; 32 | return ( 33 | 43 | ); 44 | }; 45 | 46 | export default MonitorButton; 47 | -------------------------------------------------------------------------------- /client/components/Nav.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | const Nav = () => { 5 | const [toggle, setToggle] = useState(false); 6 | 7 | return ( 8 |
9 | setToggle(!toggle)} 15 | /> 16 | 17 | 20 | 21 |
 
22 | 23 | 84 |
85 | ); 86 | }; 87 | 88 | export default Nav; 89 | -------------------------------------------------------------------------------- /client/components/Row.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // eslint-disable-next-line react/prop-types 4 | function Row({ log, index }) { 5 | return ( 6 | 7 | ▼ 8 | {log} 9 | 10 | ); 11 | } 12 | 13 | export default Row; 14 | -------------------------------------------------------------------------------- /client/components/SelectBox.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import InputLabel from '@mui/material/InputLabel'; 3 | import MenuItem from '@mui/material/MenuItem'; 4 | import FormControl from '@mui/material/FormControl'; 5 | import Select from '@mui/material/Select'; 6 | 7 | export default function SelectVariants({ 8 | optionsArray, 9 | labelText, 10 | styleProp, 11 | handleChange, 12 | valueProp, 13 | inputLabelId, 14 | selectId, 15 | requiredProp, 16 | }) { 17 | return ( 18 | 26 | {labelText} 27 | 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /client/components/SideBar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | const SideBar = () => { 5 | return ( 6 | 23 | ); 24 | }; 25 | 26 | export default SideBar; 27 | -------------------------------------------------------------------------------- /client/components/SimpleTable.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Table from '@mui/material/Table'; 3 | import TableBody from '@mui/material/TableBody'; 4 | import TableCell from '@mui/material/TableCell'; 5 | import TableContainer from '@mui/material/TableContainer'; 6 | import TableHead from '@mui/material/TableHead'; 7 | import TableRow from '@mui/material/TableRow'; 8 | import Paper from '@mui/material/Paper'; 9 | 10 | export default function SimpleTable({ rows, title, alignment }) { 11 | return ( 12 | 13 | 19 | 20 | 21 | 22 | {title} 23 | 24 | 25 | 26 | 27 | {rows.map((row) => ( 28 | 32 | 37 | {row} 38 | 39 | 40 | ))} 41 | 42 |
43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /client/components/UserLineChart.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import '@toast-ui/chart/dist/toastui-chart.min.css'; 3 | import { LineChart } from '@toast-ui/react-chart'; 4 | import axios from 'axios'; 5 | 6 | const options = { 7 | chart: { 8 | width: 'auto', 9 | height: 'auto', 10 | title: 'Log Count Per Hour', 11 | }, 12 | yAxis: { 13 | title: 'Logs', 14 | }, 15 | xAxis: { 16 | title: 'Hour', 17 | }, 18 | }; 19 | 20 | const containerStyle = { 21 | width: '100%', 22 | height: '50rem', 23 | }; 24 | 25 | const UserLineChart = ({ lastChosenIndexPattern }) => { 26 | const [data, setData] = useState(null); 27 | useEffect(() => { 28 | axios 29 | .get('/logs/hourbuckets', { 30 | params: { 31 | indexPattern: lastChosenIndexPattern, 32 | start: 'now-14d/d', 33 | end: 'now/d', 34 | }, 35 | }) 36 | .then((results) => { 37 | const buckets = results.data; 38 | const categories = buckets.map( 39 | (bucket) => 40 | new Date(bucket.key_as_string).toLocaleDateString() + 41 | ' ' + 42 | new Date(bucket.key_as_string).toLocaleTimeString() 43 | ); 44 | const seriesData = buckets.map((bucket) => bucket.doc_count); 45 | const series = [ 46 | { 47 | name: 'Logs', 48 | data: seriesData, 49 | }, 50 | ]; 51 | setData({ categories, series }); 52 | }) 53 | .catch((error) => 54 | console.log('Error in UserLineChart useEffect: ', error) 55 | ); 56 | }, [lastChosenIndexPattern]); 57 | 58 | return ( 59 | data && ( 60 |
61 | 62 |
63 | ) 64 | ); 65 | }; 66 | 67 | export default UserLineChart; 68 | -------------------------------------------------------------------------------- /client/containers/AlertsContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Alerts from '../components/Alerts'; 3 | import CreateAlert from '../components/CreateAlert'; 4 | import AlertSearchBox from '../components/AlertSearchBox'; 5 | 6 | const AlertsContainer = () => { 7 | return ( 8 |
9 |
Manage Alerts
10 |
11 |
12 |

Configure a new email alert:

13 |
14 | 18 |
19 |

Type an alert name to filter existing alerts:

20 |
21 |
22 | 27 |
28 | 29 |
30 |
31 | ); 32 | }; 33 | 34 | export default AlertsContainer; 35 | -------------------------------------------------------------------------------- /client/containers/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import SideBar from '../components/SideBar'; 3 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; 4 | import LogsContainer from './LogsContainer'; 5 | import HomePage from '../components/HomePage'; 6 | import AlertsContainer from './AlertsContainer'; 7 | import CreateIndex from './CreateIndex'; 8 | import Visualizer from './Visualizer'; 9 | import MonitorButton from '../components/MonitorButton'; 10 | import Nav from '../components/Nav'; 11 | import { useRecoilValue, useRecoilState } from 'recoil'; 12 | import { intervalIdsState, currentAlertsState } from '../atom'; 13 | import axios from 'axios'; 14 | import img from '../assets/Hermes-A-Gold.png'; 15 | 16 | const App = () => { 17 | const intervalIds = useRecoilValue(intervalIdsState); 18 | const [currentAlerts, setCurrentAlerts] = useRecoilState(currentAlertsState); 19 | 20 | useEffect(() => { 21 | axios 22 | .get('/alerts') 23 | .then((result) => { 24 | setCurrentAlerts(result.data); 25 | }) 26 | .catch((error) => console.log('Error in App alerts get request ', error)); 27 | return () => { 28 | for (const intervalId of intervalIds) { 29 | clearInterval(intervalId); 30 | } 31 | }; 32 | }, []); 33 | 34 | return ( 35 | 36 |
37 |
38 | 39 |
40 |
41 |
46 |
47 | 48 |
49 |
50 |
51 | {false && } 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 |
71 |
72 | ); 73 | }; 74 | 75 | export default App; 76 | -------------------------------------------------------------------------------- /client/containers/CreateIndex.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import SimpleTable from '../components/SimpleTable'; 3 | import SelectBox from '../components/SelectBox'; 4 | import { TextField, Button } from '@mui/material'; 5 | import { indexPatternsState, lastChosenIndexPatternState } from '../atom'; 6 | import { useRecoilState } from 'recoil'; 7 | import axios from 'axios'; 8 | 9 | const CreateIndex = () => { 10 | const [indexPatterns, setIndexPatterns] = useRecoilState(indexPatternsState); 11 | const [lastChosenIndexPattern, setLastChosenIndexPattern] = useRecoilState( 12 | lastChosenIndexPatternState 13 | ); 14 | const [alias, setAlias] = useState([]); 15 | 16 | const [input, setInput] = useState(''); 17 | 18 | useEffect(() => { 19 | axios 20 | .get('/indexpatterns') 21 | .then((result) => setIndexPatterns(result.data)) 22 | .catch((error) => console.log('Error in CreateAlert useEffect: ', error)); 23 | }, []); 24 | 25 | useEffect(() => { 26 | fetch('/logs/esindices') 27 | .then((res) => res.json()) 28 | .then((res) => setAlias(res)); 29 | }, []); 30 | 31 | // handle change func passed down to the index pattern select box 32 | const handleDropdownChange = (event) => { 33 | setLastChosenIndexPattern(event.target.value); 34 | }; 35 | 36 | const arr = []; 37 | for (let key in alias) { 38 | if (key[0] !== '.') arr.push(key); 39 | } 40 | arr.sort(); 41 | 42 | function poster(data) { 43 | fetch(`/indexpatterns`, { 44 | method: 'POST', 45 | headers: { 46 | 'Content-Type': 'Application/JSON', 47 | }, 48 | body: JSON.stringify(data), 49 | }) 50 | .then((resp) => resp.json()) 51 | .then((data) => { 52 | setIndexPatterns(data); 53 | setLastChosenIndexPattern(input); 54 | setInput(''); 55 | }) 56 | .catch((err) => console.log('Error in poster:', err)); 57 | } 58 | 59 | function deleter(data) { 60 | fetch(`/indexpatterns`, { 61 | method: 'DELETE', 62 | headers: { 63 | 'Content-Type': 'Application/JSON', 64 | }, 65 | body: JSON.stringify(data), 66 | }) 67 | .then((resp) => resp.json()) 68 | .then((data) => { 69 | setIndexPatterns(data); 70 | setLastChosenIndexPattern(''); 71 | }) 72 | 73 | .catch((err) => console.log('Error in deleter function:', err)); 74 | } 75 | 76 | function truer(arr, input) { 77 | for (let i = 0; i < arr.length; i++) { 78 | if (arr[i].includes(input) || input.charAt(input.length - 1) === '*') 79 | return true; 80 | } 81 | return false; 82 | } 83 | 84 | return ( 85 |
86 |
Manage Index Patterns
87 |
88 |
89 |

90 | Add a new index pattern to query multiple Elasticsearch indices: 91 |

92 |
93 |
94 | setInput(e.target.value)} 108 | /> 109 | {truer(arr, input) && ( 110 | 122 | )} 123 |
124 |
125 |

Delete an index pattern:

126 |
127 |
128 | 142 | 154 |
155 |
156 | {arr && ( 157 | 162 | )} 163 |
164 |
165 |
166 | ); 167 | }; 168 | 169 | export default CreateIndex; 170 | -------------------------------------------------------------------------------- /client/containers/LogsContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { useRecoilState, useRecoilValue } from 'recoil'; 3 | import { logsState } from '../atom'; 4 | import { indexPatternsState, lastChosenIndexPatternState } from '../atom'; 5 | import { TextField, Button } from '@mui/material'; 6 | import SelectBox from '../components/SelectBox'; 7 | import SimpleTable from '../components/SimpleTable'; 8 | import axios from 'axios'; 9 | 10 | const LogsContainer = () => { 11 | const [indexPatterns, setIndexPatterns] = useRecoilState(indexPatternsState); 12 | const [lastChosenIndexPattern, setLastChosenIndexPattern] = useRecoilState( 13 | lastChosenIndexPatternState 14 | ); 15 | const [logs, setLogs] = useRecoilState(logsState); 16 | const [field, setField] = useState(''); 17 | const [value, setValue] = useState(''); 18 | // handle change func passed down to the index pattern select box 19 | const handleDropdownChange = (event) => { 20 | setLastChosenIndexPattern(event.target.value); 21 | }; 22 | const getLogs = () => { 23 | if (lastChosenIndexPattern) { 24 | let fieldText = field; 25 | let valueText = value; 26 | if (fieldText === '') fieldText = '*'; 27 | if (valueText === '') valueText = '*'; 28 | axios 29 | .get( 30 | `/logs/logsbyindex/?index=${lastChosenIndexPattern}&field=${fieldText}&value=${valueText}` 31 | ) 32 | .then((res) => { 33 | setLogs(res.data.hits.hits); 34 | }) 35 | .catch((error) => { 36 | console.log('Error in getLogs get request: ', error); 37 | }); 38 | } 39 | }; 40 | 41 | useEffect(() => { 42 | axios 43 | .get('/indexpatterns') 44 | .then((result) => { 45 | setIndexPatterns(result.data); 46 | setLastChosenIndexPattern(result.data[0]); 47 | }) 48 | .catch((error) => console.log('Error in Visualizer useEffect: ', error)); 49 | }, []); 50 | 51 | useEffect(getLogs, [lastChosenIndexPattern]); 52 | 53 | const data1 = [...JSON.parse(JSON.stringify(logs))]; 54 | const seter = new Set(); 55 | const arr = []; 56 | // build an array of objects. Each object has one property, Log. The value is the _source object stringified. 57 | for (let i = 0; i < data1.length; i++) { 58 | if (data1[i]) { 59 | let string = ''; 60 | for (const key in data1[i]._source) { 61 | seter.add(key); 62 | string += `${key}: `; 63 | if (typeof data1[i]._source[key] === 'object') { 64 | string += `${JSON.stringify(data1[i]._source[key])}, `; 65 | } else { 66 | string += `${data1[i]._source[key]}, `; 67 | } 68 | } 69 | arr.push(string); 70 | } 71 | } 72 | 73 | const selectArr = Array.from(seter); 74 | return ( 75 |
76 |
View Logs
77 |
78 |
79 |

Select an index pattern:

80 |
81 |
82 | 95 |
96 |
97 |

98 | Filter logs by inputting a value that must be included in the 99 | selected field: 100 |

101 |
102 |
103 | setField(e.target.value)} 109 | styleProp={{ 110 | width: '15rem', 111 | selfAlign: 'center', 112 | marginTop: '.8rem', 113 | }} 114 | inputLabelId='field-dropdown-label' 115 | selectId='field-dropdown' 116 | /> 117 | { 132 | setValue(e.target.value); 133 | }} 134 | /> 135 | 147 |
148 |
149 |
150 |
151 |
152 | {arr && ( 153 | 154 | )} 155 |
156 |
157 |
158 |
159 |
160 | ); 161 | }; 162 | 163 | export default LogsContainer; 164 | -------------------------------------------------------------------------------- /client/containers/Visualizer.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import React, { useEffect } from 'react'; 3 | import UserLineChart from '../components/UserLineChart'; 4 | import { useRecoilState } from 'recoil'; 5 | import { indexPatternsState, lastChosenIndexPatternState } from '../atom'; 6 | import SelectBox from '../components/SelectBox'; 7 | import axios from 'axios'; 8 | 9 | const Visualizer = () => { 10 | const [indexPatterns, setIndexPatterns] = useRecoilState(indexPatternsState); 11 | const [lastChosenIndexPattern, setLastChosenIndexPattern] = useRecoilState( 12 | lastChosenIndexPatternState 13 | ); 14 | // handle change func passed down to the index pattern select box 15 | const handleDropdownChange = (event) => { 16 | setLastChosenIndexPattern(event.target.value); 17 | }; 18 | 19 | useEffect(() => { 20 | axios 21 | .get('/indexpatterns') 22 | .then((result) => setIndexPatterns(result.data)) 23 | .catch((error) => console.log('Error in Visualizer useEffect: ', error)); 24 | }, []); 25 | 26 | return ( 27 |
28 |
Visualize Logs
29 |
30 |
31 |

Select an index pattern:

32 |
33 |
34 | 48 |
49 | 50 |
51 |
52 | ); 53 | }; 54 | 55 | export default Visualizer; 56 | -------------------------------------------------------------------------------- /client/hooks/useAxios.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import { useRecoilState } from 'recoil'; 3 | import React, { useState, useEffect } from 'react'; 4 | import axios from 'axios'; 5 | import { logState } from '../atom'; 6 | export const useAxios = (url) => { 7 | const [state, setState] = useRecoilState(logState); 8 | useEffect(() => { 9 | const CancelToken = axios.CancelToken; 10 | const source = CancelToken.source(); 11 | const loadData = () => { 12 | try { 13 | axios.get(url, { cancelToken: source.token }).then((res) => { 14 | setState(res.data.hits.hits); 15 | }); 16 | } catch (error) { 17 | if (axios.isCancel(error)) { 18 | console.log('Error in useAxios useEffect: cancelled. error: ', error); 19 | } else { 20 | throw error; 21 | } 22 | } 23 | }; 24 | 25 | loadData(); 26 | return () => { 27 | source.cancel(); 28 | }; 29 | }, [url, setState]); 30 | return state; 31 | }; 32 | -------------------------------------------------------------------------------- /client/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './containers/App'; 4 | import { RecoilRoot } from 'recoil'; 5 | import { StyledEngineProvider } from '@mui/material/styles'; 6 | import './styles/main.scss'; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | 12 | 13 | 14 | 15 | , 16 | document.getElementById('root') 17 | ); 18 | -------------------------------------------------------------------------------- /client/monitor-funcs/monitorFunc.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import sendEmail from '../../email_smtp'; 3 | import Mustache from 'mustache'; 4 | // This function will send search queries to elasticsearch at a frequency defined in the alert input 5 | const monitorFunc = (alert) => { 6 | const countThreshold = 1; 7 | return setInterval(() => { 8 | axios 9 | .get('/logs/monitor', { 10 | params: { 11 | index: alert.indexPattern, 12 | query: JSON.parse(alert.editorContents), 13 | }, 14 | }) 15 | .then((results) => { 16 | if (Number(results.data.hits.total.value) >= countThreshold) { 17 | // render the new string with the mustache variables and save as emailBody 18 | const emailBody = Mustache.render( 19 | alert.emailBody, 20 | results.data.hits.hits[0]._source 21 | ); 22 | sendEmail(alert.emailAddress, alert.emailSubject, emailBody); 23 | } 24 | }) 25 | .catch((error) => { 26 | console.log('Error in monitorFunc get request: ', error); 27 | }); 28 | }, alert.monitorFrequency); 29 | }; 30 | 31 | export default monitorFunc; 32 | -------------------------------------------------------------------------------- /client/styles/_alerts.scss: -------------------------------------------------------------------------------- 1 | .page-header { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | justify-content: center; 6 | height: 6rem; 7 | font-size: xx-large; 8 | color: var(--color-primary-dark); 9 | } 10 | 11 | .alert-inputs { 12 | display: flex; 13 | justify-content: center; 14 | margin-top: 2rem; 15 | margin-left: 3rem; 16 | align-self: flex-start; 17 | } 18 | 19 | .white-box { 20 | display: flex; 21 | flex-direction: column; 22 | align-items: center; 23 | background-color: white; 24 | border-radius: 3px; 25 | box-shadow: 0 1.5rem 4rem rgba(black, 0.15); 26 | min-width: 80rem; 27 | } 28 | 29 | .editor-container-div { 30 | border: 1px solid #3c9eff; 31 | } 32 | 33 | .create-alert-details { 34 | display: flex; 35 | justify-content: space-between; 36 | } 37 | 38 | .action-details { 39 | max-width: 100%; 40 | display: flex; 41 | justify-content: space-between; 42 | } 43 | 44 | .create-alert-input { 45 | width: 20rem; 46 | min-width: 5rem; 47 | } 48 | 49 | #alertName #emailAddress { 50 | margin-right: 0.5rem; 51 | } 52 | 53 | #alerts-page { 54 | display: flex; 55 | flex-direction: column; 56 | align-items: center; 57 | padding-bottom: 4rem; 58 | } 59 | 60 | .text-and-button { 61 | display: flex; 62 | align-items: center; 63 | align-self: flex-start; 64 | } 65 | -------------------------------------------------------------------------------- /client/styles/_base.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap'); 2 | 3 | :root { 4 | //base website color scheme 5 | --color-primary-light: #3c9eff; 6 | --color-primary-dark: #4747ca; 7 | 8 | //base colors for feature cards 9 | --color-card-primary: #8d83cf; 10 | 11 | //text colors 12 | --color-grey-light-1: #faf9f9; 13 | --color-grey-light-2: #f4f2f2; 14 | --color-grey-light-3: #e9e9e9; 15 | --color-primary-gold: #d39b0e; 16 | 17 | //black 18 | --color-black: rgb(29, 29, 29); 19 | //shadows 20 | --box-shadow: 0 2rem 5rem rgba(0, 0, 0, 0.2); 21 | } 22 | 23 | * { 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | *, 29 | *::before, 30 | *::after { 31 | box-sizing: inherit; 32 | } 33 | 34 | html { 35 | box-sizing: border-box; 36 | font-size: 62.5%; 37 | font-family: 'Roboto', sans-serif; 38 | } 39 | 40 | body { 41 | color: var(--color-grey-light-1); 42 | background-image: linear-gradient( 43 | to right bottom, 44 | var(--color-primary-light), 45 | var(--color-primary-dark) 46 | ); 47 | background-size: cover; 48 | background-repeat: no-repeat; 49 | min-height: 100vh; 50 | display: flex; 51 | flex-direction: column; 52 | justify-content: space-between; 53 | } 54 | 55 | p { 56 | color: var(--color-black); 57 | font-size: 1.6rem; 58 | } 59 | 60 | .link { 61 | color: var(--color-grey-light-1); 62 | margin: 2rem 1rem; 63 | &:hover { 64 | color: var(--color-grey-light-1); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /client/styles/_createindex.scss: -------------------------------------------------------------------------------- 1 | .index-title { 2 | color: var(--color-black); 3 | font-size: 1.4rem; 4 | } 5 | 6 | .indices { 7 | color: var(--color-black); 8 | border: 1px solid var(--color-black); 9 | width: 100%; 10 | } 11 | 12 | .sources-container { 13 | display: flex; 14 | flex-direction: column; 15 | align-items: center; 16 | color: var(--color-black); 17 | padding-left: 3rem; 18 | padding-right: 3rem; 19 | padding-top: 2rem; 20 | padding-bottom: 4rem; 21 | min-width: 90rem; 22 | font-size: 0.8rem; 23 | } 24 | 25 | .index-pattern-input { 26 | display: flex; 27 | align-items: center; 28 | align-self: flex-start; 29 | margin-left: 3rem; 30 | } 31 | -------------------------------------------------------------------------------- /client/styles/_layout.scss: -------------------------------------------------------------------------------- 1 | .app-container { 2 | max-width: 100rem; 3 | margin: 3rem auto; 4 | background-color: var(--color-grey-light-3); 5 | box-shadow: var(--box-shadow); 6 | flex-wrap: wrap; 7 | border-radius: 3px; 8 | display: flex; 9 | flex-direction: column; 10 | align-items: center; 11 | padding-left: 5rem; 12 | padding-right: 5rem; 13 | } 14 | 15 | .header { 16 | height: 7rem; 17 | background-color: var(--color-black); 18 | border-bottom: var(--color-grey-light-2); 19 | flex: 0 1 50%; 20 | display: flex; 21 | justify-content: space-between; 22 | align-items: center; 23 | h1 { 24 | color: var(--color-primary-gold); 25 | font-size: 4rem; 26 | font-weight: 500; 27 | } 28 | img { 29 | height: 4rem; 30 | margin: 0 1rem; 31 | } 32 | #title { 33 | margin: 0 1rem; 34 | } 35 | } 36 | 37 | .header-buttons { 38 | margin-right: 3rem; 39 | } 40 | 41 | #hidden-button { 42 | z-index: -1; 43 | margin-left: 3rem; 44 | margin-right: 0; 45 | } 46 | 47 | .sidebar-nav { 48 | display: flex; 49 | flex-direction: column; 50 | justify-content: flex-start; 51 | 52 | :link, 53 | :visited { 54 | text-transform: uppercase; 55 | text-decoration: none; 56 | padding: 1.5rem 4rem; 57 | border-radius: 10%; 58 | transition: all 0.2s; 59 | position: relative; 60 | font-size: 1.6rem; 61 | color: #000; 62 | } 63 | } 64 | 65 | // .features{ 66 | // display: flex; 67 | // justify-self: flex-end; 68 | // color: gold; 69 | // font-size: 1.6rem; 70 | // } 71 | 72 | .home { 73 | display: flex; 74 | flex-wrap: wrap; 75 | } 76 | 77 | .filter-box { 78 | display: flex; 79 | align-items: center; 80 | width: 100%; 81 | margin-left: 3rem; 82 | } 83 | 84 | .graphic-element { 85 | min-width: 80rem; 86 | padding-left: 3rem; 87 | padding-right: 3rem; 88 | padding-bottom: 3rem; 89 | } 90 | 91 | .outer-page { 92 | margin-bottom: 4rem; 93 | } 94 | -------------------------------------------------------------------------------- /client/styles/_logs.scss: -------------------------------------------------------------------------------- 1 | // .logs-fields{ 2 | // position: relative; 3 | // font-size: 1rem; 4 | // outline: .2rem black solid; 5 | // width: 50%; 6 | // left: 30%; 7 | // top: -50%; 8 | // z-index: 1; 9 | // } 10 | 11 | .logs { 12 | display: flex; 13 | flex-direction: column; 14 | align-items: center; 15 | } 16 | 17 | #table-div { 18 | display: flex; 19 | flex-direction: column; 20 | align-items: center; 21 | } 22 | 23 | #logs-container { 24 | color: gold; 25 | width: auto; 26 | display: flex; 27 | flex-direction: column; 28 | align-items: center; 29 | } 30 | 31 | .logs-page-container { 32 | display: flex; 33 | flex-direction: column; 34 | align-items: center; 35 | } 36 | 37 | th, 38 | td { 39 | border: 1px solid var(--color-black); 40 | border-collapse: collapse; 41 | border-radius: 3px; 42 | color: var(--color-black); 43 | font-size: 1.6rem; 44 | word-break: break-all; 45 | width: auto; 46 | padding: 0.2rem; 47 | } 48 | table { 49 | padding: 1rem; 50 | box-shadow: 0 1.5rem 4rem rgba(black, 0.15); 51 | background-color: white; 52 | width: 80%; 53 | margin-top: 2rem; 54 | border-radius: 3px; 55 | } 56 | .more-info { 57 | width: fit-content; 58 | word-break: normal; 59 | } 60 | .more-info-button { 61 | text-align: center; 62 | } 63 | -------------------------------------------------------------------------------- /client/styles/_navigation.scss: -------------------------------------------------------------------------------- 1 | .navigation { 2 | &__checkbox { 3 | display: none; 4 | transition: all 0.2s; 5 | } 6 | 7 | &__button { 8 | background-color: var(--color-black); 9 | height: 6rem; 10 | width: 6rem; 11 | position: fixed; 12 | top: 0.5rem; 13 | left: 3rem; 14 | border-radius: 50%; 15 | z-index: 2000; 16 | box-shadow: 0 1rem 3rem rgba(black, 0.1); 17 | text-align: center; 18 | cursor: pointer; 19 | } 20 | 21 | &__background { 22 | height: 6rem; 23 | width: 6rem; 24 | border-radius: 50%; 25 | border-style: hidden; 26 | position: fixed; 27 | top: 0.5rem; 28 | left: 3rem; 29 | background-image: radial-gradient( 30 | var(--color-primary-light), 31 | var(--color-primary-dark) 32 | ); 33 | z-index: 1000; 34 | transition: transform 0.8s cubic-bezier(0.86, 0, 0.07, 1); 35 | 36 | //transform: scale(80); 37 | } 38 | 39 | &__nav { 40 | height: 100vh; 41 | position: fixed; 42 | left: 0; 43 | z-index: 1500; 44 | 45 | opacity: 0; 46 | width: 0; 47 | transition: all 0.8s cubic-bezier(0.68, -0.55, 0.265, 1.55); 48 | } 49 | 50 | &__list { 51 | position: absolute; 52 | top: 50%; 53 | left: 50%; 54 | transform: translate(-50%, -50%); 55 | list-style: none; 56 | text-align: center; 57 | width: 100%; 58 | } 59 | 60 | &__item { 61 | margin: 1rem; 62 | } 63 | 64 | &__link { 65 | &:link, 66 | &:visited { 67 | display: inline-block; 68 | font-size: 3rem; 69 | font-weight: 300; 70 | padding: 1rem 2rem; 71 | color: white; 72 | text-decoration: none; 73 | text-transform: uppercase; 74 | background-image: linear-gradient( 75 | 120deg, 76 | transparent 0%, 77 | transparent 50%, 78 | white 50% 79 | ); 80 | background-size: 220%; 81 | transition: all 0.4s; 82 | 83 | span { 84 | margin-right: 1.5rem; 85 | display: inline-block; 86 | } 87 | } 88 | 89 | &:hover, 90 | &:active { 91 | background-position: 100%; 92 | color: var(--color-primary-light); 93 | transform: translateX(1rem); 94 | } 95 | } 96 | 97 | //FUNCTIONALITY 98 | &__checkbox:checked ~ &__background { 99 | transform: scale(80); 100 | } 101 | 102 | &__checkbox:checked ~ &__nav { 103 | opacity: 1; 104 | width: 100%; 105 | } 106 | 107 | //ICON 108 | &__icon { 109 | position: relative; 110 | margin-top: 3rem; 111 | 112 | &, 113 | &::before, 114 | &::after { 115 | width: 3rem; 116 | height: 2px; 117 | //three lines color below 118 | background-color: var(--color-grey-light-3); 119 | display: inline-block; 120 | } 121 | 122 | &::before, 123 | &::after { 124 | content: ''; 125 | position: absolute; 126 | left: 0; 127 | transition: all 0.2s; 128 | } 129 | 130 | &::before { 131 | top: -0.8rem; 132 | } 133 | &::after { 134 | top: 0.8rem; 135 | } 136 | } 137 | 138 | &__button:hover &__icon::before { 139 | top: -1rem; 140 | } 141 | 142 | &__button:hover &__icon::after { 143 | top: 1rem; 144 | } 145 | 146 | &__checkbox:checked + &__button &__icon { 147 | background-color: transparent; 148 | } 149 | 150 | &__checkbox:checked + &__button &__icon::before { 151 | top: 0; 152 | transform: rotate(135deg); 153 | } 154 | 155 | &__checkbox:checked + &__button &__icon::after { 156 | top: 0; 157 | transform: rotate(-135deg); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /client/styles/_sidebar.scss: -------------------------------------------------------------------------------- 1 | .sidebar-nav{ 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: flex-start; 5 | 6 | 7 | :link, 8 | :visited{ 9 | text-transform: uppercase; 10 | text-decoration: none; 11 | padding: 1.5rem 4rem; 12 | display: inline-block; 13 | border-radius: 10%; 14 | transition: all .2s; 15 | position: relative; 16 | font-size: 1.6rem; 17 | color: #000; 18 | } 19 | 20 | 21 | } -------------------------------------------------------------------------------- /client/styles/_visualizer.scss: -------------------------------------------------------------------------------- 1 | #visualizer-white-box { 2 | min-width: 90rem; 3 | } 4 | 5 | .chart { 6 | padding-left: 2rem; 7 | padding-right: 1rem; 8 | padding-bottom: 1rem; 9 | width: 100%; 10 | border-radius: 3px; 11 | } 12 | -------------------------------------------------------------------------------- /client/styles/components/_button.scss: -------------------------------------------------------------------------------- 1 | .btn{ 2 | 3 | &:link, 4 | &:visited{ 5 | text-transform: uppercase; 6 | text-decoration: none; 7 | padding: 1.5rem 4rem; 8 | display: inline-block; 9 | border-radius: 10rem; 10 | transition: all .2s; 11 | position: relative; 12 | font-size: 1.6rem; 13 | } 14 | &:hover{ 15 | transform: translateY(-3px); 16 | box-shadow: 0 1rem 2rem rgba(black, 0.2); 17 | &::after{ 18 | transform: scaleX(1.4) scaleY(1.6); 19 | opacity: 0; 20 | } 21 | } 22 | &:active{ 23 | transform: translateY(-1px); 24 | box-shadow: 0 .5rem 1rem rgba(black, 0.2); 25 | } 26 | &--white { 27 | background-color: white; 28 | color: grey; 29 | &::after { 30 | background-color:white 31 | } 32 | } 33 | &--green { 34 | background-color: var(--color-primary-light); 35 | color: white; 36 | &::after { 37 | background-color:var(--color-primary-light); 38 | } 39 | } 40 | 41 | &::after{ 42 | content: ""; 43 | display: inline-block; 44 | height: 100%; 45 | width: 100%; 46 | border-radius: 10rem; 47 | position: absolute; 48 | top: 0; 49 | left: 0; 50 | z-index: -1; 51 | transition: all .4s; 52 | } 53 | &--animated{ 54 | animation: moveInBottom .5s ease-out .75s; 55 | animation-fill-mode: backwards; 56 | } 57 | 58 | } 59 | 60 | .btn-text { 61 | &:link, 62 | &:visited{ 63 | font-size: 1.6rem; 64 | color: var(--color-primary-light); 65 | display: inline-block; 66 | text-decoration: none; 67 | border-bottom: 1px solid var(--color-primary-light); 68 | padding: 3px; 69 | transition: all .2s 70 | } 71 | &:hover{ 72 | background-color:var(--color-primary-light); 73 | color: white; 74 | box-shadow: 0 1rem 2rem rgba(black, .15); 75 | transform: translateY(-2px) 76 | } 77 | &:active{ 78 | box-shadow: 0 1rem 2rem rgba(black, .15); 79 | transform: translateY(0) 80 | } 81 | } -------------------------------------------------------------------------------- /client/styles/components/_card.scss: -------------------------------------------------------------------------------- 1 | .card { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | text-decoration: none; 6 | // FUNCTIONALITY 7 | perspective: 150rem; 8 | -moz-perspective: 150rem; 9 | position: relative; 10 | height: 40rem; 11 | width: 20rem; 12 | padding: 5rem; 13 | &__side { 14 | height: 40rem; 15 | display: flex; 16 | flex-direction: column; 17 | justify-content: space-around; 18 | transition: all 0.4s ease; 19 | position: absolute; 20 | top: 0; 21 | left: 0; 22 | width: 100%; 23 | backface-visibility: hidden; 24 | border-radius: 3px; 25 | overflow: hidden; 26 | box-shadow: 0 1.5rem 4rem rgba(black, 0.15); 27 | 28 | &--front { 29 | background-color: white; 30 | background-image: linear-gradient( 31 | to right bottom, 32 | var(--color-card-primary), 33 | var(--color-primary-light) 34 | ); 35 | } 36 | 37 | // &--back { 38 | // transform: rotateY(180deg); 39 | 40 | // // &-1 { 41 | // // background-image: linear-gradient(to right bottom, $color-secondary-light, $color-secondary-dark); 42 | // // } 43 | 44 | // &-2 { 45 | // background-image: linear-gradient( 46 | // to right bottom, 47 | // var(--color-primary-light), 48 | // var(--color-card-primary) 49 | // ); 50 | // } 51 | 52 | // // &-3 { 53 | // // background-image: linear-gradient(to right bottom, $color-tertiary-light, $color-tertiary-dark); 54 | // // } 55 | // } 56 | } 57 | 58 | &:hover &__side--front { 59 | transform: scale(1.03); 60 | } 61 | 62 | // FRONT SIDE STYLING 63 | 64 | &__heading { 65 | font-size: 2.8rem; 66 | font-weight: 300; 67 | text-transform: uppercase; 68 | text-align: center; 69 | color: white; 70 | height: 7rem; 71 | } 72 | 73 | &__heading-span { 74 | padding: 1rem 1.5rem; 75 | -webkit-box-decoration-break: clone; 76 | box-decoration-break: clone; 77 | 78 | // &--1 { 79 | // background-image: linear-gradient(to right bottom, 80 | // rgba($color-secondary-light, .85), 81 | // rgba($color-secondary-dark, .85)); 82 | // } 83 | 84 | &--2 { 85 | background-image: linear-gradient( 86 | to right bottom, 87 | rgba(var(--color-primary-light), 0.85), 88 | rgba(var(--color-primary-dark), 0.85) 89 | ); 90 | } 91 | 92 | // &--3 { 93 | // background-image: linear-gradient(to right bottom, 94 | // rgba($color-tertiary-light, .85), 95 | // rgba($color-tertiary-dark, .85)); 96 | // } 97 | } 98 | 99 | &__picture { 100 | display: flex; 101 | flex-direction: column; 102 | align-items: center; 103 | } 104 | 105 | &__details { 106 | padding: 2rem; 107 | text-decoration: none; 108 | height: 15rem; 109 | 110 | ul { 111 | list-style: none; 112 | width: 95%; 113 | margin: 0 auto; 114 | 115 | li { 116 | text-align: center; 117 | font-size: 1.5rem; 118 | padding: 1rem; 119 | 120 | &:not(:last-child) { 121 | border-bottom: 1px solid var(--color-grey-light-1); 122 | } 123 | } 124 | } 125 | } 126 | 127 | // FRONT SIDE STYLING 128 | &__cta { 129 | position: absolute; 130 | top: 50%; 131 | left: 50%; 132 | transform: translate(-50%, -50%); 133 | width: 90%; 134 | } 135 | 136 | &__price-box { 137 | text-align: center; 138 | color: white; 139 | margin-bottom: 8rem; 140 | } 141 | 142 | &__price-only { 143 | font-size: 1.4rem; 144 | text-transform: uppercase; 145 | } 146 | 147 | &__price-value { 148 | font-size: 6rem; 149 | font-weight: 100; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /client/styles/main.scss: -------------------------------------------------------------------------------- 1 | @import "./base.scss"; 2 | @import "./layout.scss"; 3 | @import "./navigation"; 4 | @import "./components/card"; 5 | @import "./components/button"; 6 | @import "./visualizer"; 7 | @import "./logs"; 8 | @import "./alerts"; 9 | @import "./createindex"; 10 | -------------------------------------------------------------------------------- /email_smtp.js: -------------------------------------------------------------------------------- 1 | function sendEmail(recipient, subject, body) { 2 | Email.send({ 3 | Host: 'smtp.gmail.com', 4 | Username: '', 5 | Password: '', 6 | To: recipient, 7 | From: '', 8 | Subject: subject, 9 | Body: body, 10 | }); 11 | } 12 | export default sendEmail; 13 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Hermes 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hermes", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.jsx", 6 | "scripts": { 7 | "test": "jest --verbose", 8 | "start": "node server/server.js", 9 | "build": "NODE_ENV=production webpack", 10 | "dev": "concurrently \"cross-env NODE_ENV=development webpack serve --open\" \"cross-env NODE_ENV=development nodemon server/server.js --ignore '*.json'\"" 11 | }, 12 | "nodemonConfig": { 13 | "ignore": [ 14 | "build", 15 | "client" 16 | ] 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/oslabs-beta/hermes.git" 21 | }, 22 | "author": "", 23 | "license": "ISC", 24 | "bugs": { 25 | "url": "https://github.com/oslabs-beta/hermes/issues" 26 | }, 27 | "homepage": "https://github.com/oslabs-beta/hermes#readme", 28 | "dependencies": { 29 | "@emotion/react": "^11.4.1", 30 | "@emotion/styled": "^11.3.0", 31 | "@material-ui/core": "^4.12.3", 32 | "@material-ui/icons": "^4.11.2", 33 | "@monaco-editor/react": "^4.2.2", 34 | "@mui/material": "^5.0.1", 35 | "@mui/styled-engine-sc": "^5.0.0", 36 | "@mui/styles": "^5.0.1", 37 | "@toast-ui/react-chart": "^4.3.6", 38 | "@toast-ui/react-grid": "^4.18.1", 39 | "axios": "^0.21.1", 40 | "cors": "^2.8.5", 41 | "esbuild": "^0.12.25", 42 | "express": "^4.17.1", 43 | "jest": "^27.2.1", 44 | "mustache": "^4.2.0", 45 | "node-fetch": "^3.0.0", 46 | "nodemon": "^2.0.12", 47 | "react": "^17.0.2", 48 | "react-dom": "^17.0.2", 49 | "react-router-dom": "^5.2.1", 50 | "recoil": "^0.4.1", 51 | "styled-components": "^5.3.1", 52 | "webpack": "^5.52.1" 53 | }, 54 | "devDependencies": { 55 | "@babel/core": "^7.15.0", 56 | "@babel/preset-env": "^7.15.0", 57 | "@babel/preset-react": "^7.14.5", 58 | "babel-loader": "^8.2.2", 59 | "concurrently": "^6.2.1", 60 | "cross-env": "^7.0.3", 61 | "css-loader": "^6.2.0", 62 | "eslint": "^7.32.0", 63 | "eslint-plugin-react": "^7.25.1", 64 | "file-loader": "^6.2.0", 65 | "isomorphic-fetch": "^3.0.0", 66 | "redux-devtools-extension": "^2.13.9", 67 | "sass": "^1.37.5", 68 | "sass-loader": "^12.1.0", 69 | "style-loader": "^3.2.1", 70 | "supertest": "^6.1.6", 71 | "webpack-cli": "^4.7.2", 72 | "webpack-dev-server": "^3.11.2" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /server/controllers/alertsController.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const alertsController = {}; 4 | const fetch = (...args) => 5 | import('node-fetch').then(({ default: fetch }) => fetch(...args)); 6 | 7 | const storage = path.resolve(__dirname, '../../storage/alerts.json'); 8 | 9 | alertsController.getAlerts = (req, res, next) => { 10 | res.locals.alerts = JSON.parse(fs.readFileSync(storage)); 11 | next(); 12 | }; 13 | 14 | alertsController.setAlert = (req, res, next) => { 15 | const stringifiedAlert = JSON.stringify(req.body.alert); 16 | if (!res.locals.alerts.includes(stringifiedAlert) && stringifiedAlert) { 17 | res.locals.alerts.push(stringifiedAlert); 18 | fs.writeFileSync(storage, JSON.stringify(res.locals.alerts)); 19 | next(); 20 | } else { 21 | next(); 22 | } 23 | }; 24 | 25 | alertsController.deleteAlert = (req, res, next) => { 26 | const stringifiedAlert = JSON.stringify(req.body.alert); 27 | if (res.locals.alerts.includes(stringifiedAlert)) { 28 | const arrIndex = res.locals.alerts.indexOf(stringifiedAlert); 29 | res.locals.alerts.splice(arrIndex, 1); 30 | fs.writeFileSync(storage, JSON.stringify(res.locals.alerts)); 31 | next(); 32 | } else { 33 | next(); 34 | } 35 | }; 36 | 37 | module.exports = alertsController; 38 | -------------------------------------------------------------------------------- /server/controllers/indexPatternsController.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const indexPatternsController = {}; 4 | const fetch = (...args) => 5 | import('node-fetch').then(({ default: fetch }) => fetch(...args)); 6 | 7 | const storage = path.resolve(__dirname, '../../storage/index_patterns.json'); 8 | 9 | indexPatternsController.getIndexPatterns = (req, res, next) => { 10 | res.locals.indexPatterns = JSON.parse(fs.readFileSync(storage)); 11 | next(); 12 | }; 13 | 14 | indexPatternsController.setIndexPattern = (req, res, next) => { 15 | if ( 16 | !res.locals.indexPatterns.includes(req.body.indexPattern) && 17 | req.body.indexPattern 18 | ) { 19 | res.locals.indexPatterns.push(req.body.indexPattern); 20 | fs.writeFileSync(storage, JSON.stringify(res.locals.indexPatterns)); 21 | next(); 22 | } else { 23 | next(); 24 | } 25 | }; 26 | 27 | indexPatternsController.deleteIndexPattern = (req, res, next) => { 28 | if (res.locals.indexPatterns.includes(req.body.indexPattern)) { 29 | const arrIndex = res.locals.indexPatterns.indexOf(req.body.indexPattern); 30 | res.locals.indexPatterns.splice(arrIndex, 1); 31 | fs.writeFileSync(storage, JSON.stringify(res.locals.indexPatterns)); 32 | next(); 33 | } else { 34 | next(); 35 | } 36 | }; 37 | 38 | module.exports = indexPatternsController; 39 | -------------------------------------------------------------------------------- /server/controllers/logsController.js: -------------------------------------------------------------------------------- 1 | const logsController = {}; 2 | const fetch = (...args) => 3 | import('node-fetch').then(({ default: fetch }) => fetch(...args)); 4 | 5 | logsController.getLogsByIndex = (req, res, next) => { 6 | const index = req.query.index; 7 | const field = req.query.field; 8 | const value = req.query.value; 9 | fetch(`http://localhost:9200/${index}/_search?q=${field}:${value}&size=500`) 10 | .then((data) => data.json()) 11 | .then((logs) => { 12 | res.locals.logs = logs; 13 | return next(); 14 | }) 15 | .catch((error) => { 16 | console.log(error); 17 | return next( 18 | 'Error in logsController.getLogsByIndex: Check server logs for more information.' 19 | ); 20 | }); 21 | }; 22 | 23 | logsController.getEsIndices = (req, res, next) => { 24 | fetch('http://localhost:9200/_aliases') 25 | .then((data) => data.json()) 26 | .then((indices) => { 27 | res.locals.indices = indices; 28 | return next(); 29 | }) 30 | .catch((error) => { 31 | console.log(error); 32 | return next( 33 | 'Error in logsController.getESIndices: Check server logs for more information.' 34 | ); 35 | }); 36 | }; 37 | 38 | logsController.getHourBuckets = (req, res, next) => { 39 | Date.prototype.addHours = function (h) { 40 | this.setTime(this.getTime() + h * 60 * 60 * 1000); 41 | return this; 42 | }; 43 | fetch(`http://localhost:9200/${req.query.indexPattern}/_search?size=0`, { 44 | method: 'POST', 45 | headers: { 46 | 'Content-Type': 'application/json', 47 | }, 48 | body: JSON.stringify({ 49 | query: { 50 | range: { 51 | '@timestamp': { 52 | gte: req.query.start, 53 | lte: req.query.end, 54 | }, 55 | }, 56 | }, 57 | aggs: { 58 | logs_over_time: { 59 | date_histogram: { 60 | field: '@timestamp', 61 | fixed_interval: '1h', 62 | }, 63 | }, 64 | }, 65 | }), 66 | }) 67 | .then((data) => data.json()) 68 | .then((results) => { 69 | res.locals.hourBuckets = results.aggregations.logs_over_time.buckets; 70 | return next(); 71 | }) 72 | .catch((error) => { 73 | console.log(error); 74 | return next( 75 | 'Error in logsController.getHourBuckets: Check server logs for more information.' 76 | ); 77 | }); 78 | }; 79 | 80 | logsController.getMonitorResults = (req, res, next) => { 81 | const query = JSON.parse(req.query.query); 82 | fetch(`http://localhost:9200/${req.query.index}/_search`, { 83 | method: 'POST', 84 | headers: { 85 | 'Content-Type': 'application/json', 86 | }, 87 | body: JSON.stringify({ 88 | query, 89 | }), 90 | }) 91 | .then((data) => data.json()) 92 | .then((results) => { 93 | res.locals.monitorResults = results; 94 | return next(); 95 | }) 96 | .catch((error) => { 97 | console.log(error); 98 | return next( 99 | 'Error in logsController.getMonitorResults: Check server logs for more information.' 100 | ); 101 | }); 102 | }; 103 | 104 | module.exports = logsController; 105 | -------------------------------------------------------------------------------- /server/routes/alertsRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const alertsController = require('../controllers/alertsController'); 3 | const router = express.Router(); 4 | 5 | router.get('/', alertsController.getAlerts, (req, res) => { 6 | const alerts = res.locals.alerts.map((alert) => JSON.parse(alert)); 7 | return res.status(200).json(alerts); 8 | }); 9 | 10 | router.post( 11 | '/', 12 | alertsController.getAlerts, 13 | alertsController.setAlert, 14 | (req, res) => { 15 | const alerts = res.locals.alerts.map((alert) => JSON.parse(alert)); 16 | 17 | return res.status(200).json(alerts); 18 | } 19 | ); 20 | 21 | router.delete( 22 | '/', 23 | alertsController.getAlerts, 24 | alertsController.deleteAlert, 25 | (req, res) => { 26 | const alerts = res.locals.alerts.map((alert) => JSON.parse(alert)); 27 | 28 | return res.status(200).json(alerts); 29 | } 30 | ); 31 | module.exports = router; 32 | -------------------------------------------------------------------------------- /server/routes/indexPatternsRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const indexPatternsController = require('../controllers/indexPatternsController.js'); 3 | const router = express.Router(); 4 | 5 | router.get('/', indexPatternsController.getIndexPatterns, (req, res) => { 6 | return res.status(200).json(res.locals.indexPatterns); 7 | }); 8 | 9 | router.post( 10 | '/', 11 | indexPatternsController.getIndexPatterns, 12 | indexPatternsController.setIndexPattern, 13 | (req, res) => { 14 | return res.status(200).json(res.locals.indexPatterns); 15 | } 16 | ); 17 | 18 | router.delete( 19 | '/', 20 | indexPatternsController.getIndexPatterns, 21 | indexPatternsController.deleteIndexPattern, 22 | (req, res) => { 23 | return res.status(200).json(res.locals.indexPatterns); 24 | } 25 | ); 26 | module.exports = router; 27 | -------------------------------------------------------------------------------- /server/routes/logsRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const logsController = require('../controllers/logsController.js'); 3 | const router = express.Router(); 4 | 5 | router.get('/logsbyindex', logsController.getLogsByIndex, (req, res) => { 6 | return res.status(200).json(res.locals.logs); 7 | }); 8 | router.get('/esindices', logsController.getEsIndices, (req, res) => { 9 | return res.status(200).json(res.locals.indices); 10 | }); 11 | router.get('/hourbuckets', logsController.getHourBuckets, (req, res) => { 12 | return res.status(200).json(res.locals.hourBuckets); 13 | }); 14 | router.get('/monitor', logsController.getMonitorResults, (req, res) => { 15 | return res.status(200).json(res.locals.monitorResults); 16 | }); 17 | module.exports = router; 18 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | const path = require('path'); 4 | 5 | const logsRouter = require('./routes/logsRouter.js'); 6 | const indexPatternsRouter = require('./routes/indexPatternsRouter.js'); 7 | const alertsRouter = require('./routes/alertsRouter'); 8 | // parse incoming data 9 | app.use(express.json()); 10 | app.use(express.urlencoded({ extended: true })); 11 | // send bundle 12 | app.use('/build', express.static(path.join(__dirname, '../build/'))); 13 | app.get('/', (req, res) => { 14 | return res.status(200).sendFile(path.resolve(__dirname, '../index.html')); 15 | }); 16 | 17 | app.use('/logs', logsRouter); 18 | app.use('/indexpatterns', indexPatternsRouter); 19 | app.use('/alerts', alertsRouter); 20 | 21 | //catch all route handler, handles request to an unknown route 22 | app.use((req, res) => 23 | res.status(404).send('The page you are looking for not exist.') 24 | ); 25 | 26 | //gloabal error handler 27 | app.use((err, req, res, next) => { 28 | console.log(err); 29 | return res 30 | .status(500) 31 | .send('Unknown error in middleware. See server logs for more information.'); 32 | }); 33 | 34 | app.listen(3068, () => console.log('Listening on port 3068')); 35 | -------------------------------------------------------------------------------- /storage/alerts.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /storage/index_patterns.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './client/index.jsx', 5 | output: { 6 | path: path.resolve(__dirname, './build'), 7 | filename: 'bundle.js', 8 | }, 9 | mode: process.env.NODE_ENV, 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.jsx?/, 14 | exclude: /(node_modules)/, 15 | use: { 16 | loader: 'babel-loader', 17 | options: { 18 | presets: ['@babel/preset-env', '@babel/preset-react'], 19 | }, 20 | }, 21 | }, 22 | { 23 | test: /\.css$/i, 24 | use: ['style-loader', 'css-loader'], 25 | }, 26 | { 27 | test: /\.s[ac]ss$/i, 28 | exclude: /(node_modules)/, 29 | use: ['style-loader', 'css-loader', 'sass-loader'], 30 | }, 31 | { 32 | test: /\.(ttf|eot|svg|gif|woff|woff2|jpeg|png)(\?v=[0-9]\.[0-9]\.[0-9])?$/, 33 | use: [ 34 | { 35 | loader: 'file-loader', 36 | }, 37 | ], 38 | }, 39 | ], 40 | }, 41 | devServer: { 42 | publicPath: '/build/', 43 | historyApiFallback: true, 44 | proxy: { 45 | '/': 'http://localhost:3068', 46 | }, 47 | }, 48 | resolve: { 49 | extensions: ['.jsx', '...'], 50 | }, 51 | }; 52 | --------------------------------------------------------------------------------