├── .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 |
86 |
87 |
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 |
171 | Create New Alert
172 |
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 |
324 | Cancel
325 |
326 |
331 | Create
332 |
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 |
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 | //
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 |
41 | Monitoring: {monitorStatus}
42 |
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 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | {toggle && (
25 |
26 |
27 |
28 | setToggle(!toggle)}
32 | >
33 | Dashboard
34 |
35 |
36 |
37 |
38 |
39 | setToggle(!toggle)}
43 | >
44 | Alerts
45 |
46 |
47 |
48 |
49 |
50 | setToggle(!toggle)}
54 | >
55 | View Logs
56 |
57 |
58 |
59 |
60 |
61 | setToggle(!toggle)}
65 | >
66 | Visualize Logs
67 |
68 |
69 |
70 |
71 |
72 | setToggle(!toggle)}
76 | >
77 | Index Patterns
78 |
79 |
80 |
81 |
82 | )}
83 |
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 |
34 | {optionsArray.map((option, i) => (
35 |
36 | {option}
37 |
38 | ))}
39 |
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 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
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 |
10 |
11 |
12 |
Configure a new email alert:
13 |
14 |
18 |
19 |
Type an alert name to filter existing alerts:
20 |
21 |
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 |
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 |
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 | poster({ indexPattern: input })}
119 | >
120 | Add
121 |
122 | )}
123 |
124 |
125 |
Delete an index pattern:
126 |
127 |
128 |
142 | deleter({ indexPattern: lastChosenIndexPattern })}
151 | >
152 | Delete
153 |
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 |
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 |
145 | Filter Search Results
146 |
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 |
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 |
--------------------------------------------------------------------------------