Skip to content

Commit e84a3fa

Browse files
committed
Add websocket support for testing rules. Add search endpoint for checking user queries before a test runs.
1 parent b3b73e9 commit e84a3fa

File tree

8 files changed

+82
-83
lines changed

8 files changed

+82
-83
lines changed

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ The most convenient way to run the ElastAlert server is by using our Docker cont
1414
To run the Docker image you will want to mount the volumes for configuration and rule files to keep them after container updates. In order to do that conveniently, please do: `git clone https://github.com/bitsensor/elastalert.git; cd elastalert`
1515

1616
```bash
17-
docker run -d -p 3030:3030 \
17+
docker run -d -p 3030:3030 -p 3333:3333 \
1818
-v `pwd`/config/elastalert.yaml:/opt/elastalert/config.yaml \
1919
-v `pwd`/config/config.json:/opt/elastalert-server/config/config.json \
2020
-v `pwd`/rules:/opt/elastalert/rules \
@@ -60,6 +60,7 @@ You can use the following config options:
6060
{
6161
"appName": "elastalert-server", // The name used by the logging framework.
6262
"port": 3030, // The port to bind to
63+
"wsport": 3333, // The port to bind to for websockets
6364
"elastalertPath": "/opt/elastalert", // The path to the root ElastAlert folder. It's the folder that contains the `setup.py` script.
6465
"start": "2014-01-01T00:00:00", // Optional date to start querying from
6566
"end": "2016-01-01T00:00:00", // Optional date to stop querying at
@@ -208,10 +209,10 @@ This server exposes the following REST API's:
208209
}
209210
}
210211
```
211-
212-
- **GET `/test_stream`**
213212

214-
This allows you to test a rule and get a [Server Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) event stream back. Pass params `rule` (yaml string) and `options` (JSON string) to start receiving events.
213+
- **WEBSOCKET `/test`**
214+
215+
This allows you to test a rule and receive progress over a websocket. Send a message as JSON object (stringified) with two keys: `rule` (yaml string) and `options` (JSON object). You will receive progress messages over the socket as the test runs.
215216

216217
- **GET `/metadata/:type`**
217218

config/config.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"appName": "elastalert-server",
33
"port": 3030,
44
"elastalertPath": "/opt/elastalert",
5+
"wsport": 3333,
56
"verbose": false,
67
"es_debug": false,
78
"debug": false,

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
"raven": "^2.6.1",
3636
"request": "^2.85.0",
3737
"request-promise-native": "^1.0.5",
38-
"tar": "^4.4.1"
38+
"tar": "^4.4.1",
39+
"ws": "^6.0.0"
3940
},
4041
"devDependencies": {
4142
"eslint": "^4.17.0",

src/common/websocket.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import WebSocket from 'ws';
2+
3+
export var wss = null;
4+
5+
export function listen(port) {
6+
wss = new WebSocket.Server({ port, path: '/test' });
7+
8+
wss.on('connection', ws => {
9+
ws.isAlive = true;
10+
ws.on('pong', () => {
11+
ws.isAlive = true;
12+
});
13+
});
14+
15+
return wss;
16+
}
17+
18+
// Keepalive in case clients lose connection during a long rule test.
19+
// If client doesn't respond in 10s this will close the socket and
20+
// therefore stop the elastalert test from continuing to run detached.
21+
setInterval(() => {
22+
wss.clients.forEach(ws => {
23+
if (ws.isAlive === false) return ws.terminate();
24+
ws.isAlive = false;
25+
ws.ping(() => {});
26+
});
27+
}, 10000);

src/controllers/test/index.js

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export default class TestController {
1919
});
2020
}
2121

22-
testRule(rule, options, stream, response) {
22+
testRule(rule, options, socket) {
2323
const self = this;
2424
let tempFileName = '~' + randomstring.generate() + '.temp';
2525
let tempFilePath = path.join(self.testFolder, tempFileName);
@@ -55,31 +55,41 @@ export default class TestController {
5555
break;
5656
}
5757

58+
5859
try {
5960
let testProcess = spawn('python', processOptions, {
6061
cwd: self._elastalertPath
6162
});
6263

64+
// When the websocket closes we kill the test process
65+
// so it doesn't keep running detached
66+
if (socket) {
67+
socket.on('close', () => {
68+
testProcess.kill();
69+
});
70+
}
71+
6372
testProcess.stdout.on('data', function (data) {
64-
if (stream) {
65-
response.write('event: result\ndata: ' + data.toString() + '\n\n');
73+
if (socket) {
74+
socket.send(JSON.stringify({
75+
event: 'result',
76+
data: data.toString()
77+
}));
6678
}
6779
stdoutLines.push(data.toString());
6880
});
6981

7082
testProcess.stderr.on('data', function (data) {
71-
if (stream) {
72-
response.write('event: progress\ndata: ' + data.toString() + '\n\n');
83+
if (socket) {
84+
socket.send(JSON.stringify({
85+
event: 'progress',
86+
data: data.toString()
87+
}));
7388
}
7489
stderrLines.push(data.toString());
7590
});
7691

7792
testProcess.on('exit', function (statusCode) {
78-
if (stream) {
79-
response.write('event: done\ndata: DONE\n\n');
80-
response.end();
81-
}
82-
8393
if (statusCode === 0) {
8494
if (options.format === 'json') {
8595
resolve(stdoutLines.join(''));
@@ -88,8 +98,10 @@ export default class TestController {
8898
resolve(stdoutLines.join('\n'));
8999
}
90100
} else {
91-
reject(stderrLines.join('\n'));
92-
logger.error(stderrLines.join('\n'));
101+
if (!socket) {
102+
reject(stderrLines.join('\n'));
103+
logger.error(stderrLines.join('\n'));
104+
}
93105
}
94106

95107
fileSystem.deleteFile(tempFilePath)
@@ -106,6 +118,8 @@ export default class TestController {
106118
logger.error(`Failed to write file ${tempFileName} to ${self.testFolder} with error:`, error);
107119
reject(error);
108120
});
121+
}).catch((error) => {
122+
logger.error('Failed to test rule with error:', error);
109123
});
110124
}
111125

src/elastalert_server.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import Logger from './common/logger';
44
import config from './common/config';
55
import path from 'path';
66
import FileSystem from './common/file_system';
7+
import { listen } from './common/websocket';
78
import setupRouter from './routes/route_setup';
89
import ProcessController from './controllers/process';
910
import RulesController from './controllers/rules';
@@ -77,8 +78,27 @@ export default class ElastalertServer {
7778
self._fileSystemController.createDirectoryIfNotExists(self.getDataFolder()).catch(function (error) {
7879
logger.error('Error creating data folder with error:', error);
7980
});
80-
81+
8182
logger.info('Server listening on port ' + config.get('port'));
83+
84+
let wss = listen(config.get('wsport'));
85+
86+
wss.on('connection', ws => {
87+
ws.on('message', (data) => {
88+
try {
89+
data = JSON.parse(data);
90+
if (data.rule) {
91+
let rule = data.rule;
92+
let options = data.options;
93+
self._testController.testRule(rule, options, ws);
94+
}
95+
} catch (error) {
96+
console.log(error);
97+
}
98+
});
99+
});
100+
101+
logger.info('Websocket listening on port 3333');
82102
} catch (error) {
83103
logger.error('Starting server failed with error:', error);
84104
process.exit(1);

src/handlers/test/stream.js

Lines changed: 0 additions & 59 deletions
This file was deleted.

src/routes/routes.js

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import templateGetHandler from '../handlers/templates/id/get';
1212
import templatePostHandler from '../handlers/templates/id/post';
1313
import templateDeleteHandler from '../handlers/templates/id/delete';
1414
import testPostHandler from '../handlers/test/post';
15-
import testStreamGetHandler from '../handlers/test/stream';
1615
import configGetHandler from '../handlers/config/get';
1716
import configPostHandler from '../handlers/config/post';
1817
import metadataHandler from '../handlers/metadata/get';
@@ -70,11 +69,6 @@ let routes = [
7069
method: 'POST',
7170
handler: testPostHandler
7271
},
73-
{
74-
path: 'test_stream',
75-
method: 'GET',
76-
handler: testStreamGetHandler
77-
},
7872
{
7973
path: 'config',
8074
method: ['GET', 'POST'],

0 commit comments

Comments
 (0)