Skip to content

Commit 808030b

Browse files
Merge branch 'develop' into 'master'
Develop See merge request bitsensor/back-end/elastalert!35
2 parents 4b6a372 + 671cac6 commit 808030b

File tree

15 files changed

+186
-15
lines changed

15 files changed

+186
-15
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,5 @@ lib/
7474
*.pyc
7575
config/config.json
7676
package-lock.json
77+
78+
.vscode

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
FROM alpine:latest as py-ea
2-
ARG ELASTALERT_VERSION=v0.1.38
2+
ARG ELASTALERT_VERSION=v0.1.39
33
ENV ELASTALERT_VERSION=${ELASTALERT_VERSION}
44
# URL from which to download Elastalert.
55
ARG ELASTALERT_URL=https://github.com/Yelp/elastalert/archive/$ELASTALERT_VERSION.zip

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
v ?= v0.1.38
1+
v ?= v0.1.39
22

33
all: build
44

README.md

Lines changed: 10 additions & 1 deletion
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/elastalert-test.yaml:/opt/elastalert/config-test.yaml \
2020
-v `pwd`/config/config.json:/opt/elastalert-server/config/config.json \
@@ -61,6 +61,7 @@ You can use the following config options:
6161
{
6262
"appName": "elastalert-server", // The name used by the logging framework.
6363
"port": 3030, // The port to bind to
64+
"wsport": 3333, // The port to bind to for websockets
6465
"elastalertPath": "/opt/elastalert", // The path to the root ElastAlert folder. It's the folder that contains the `setup.py` script.
6566
"start": "2014-01-01T00:00:00", // Optional date to start querying from
6667
"end": "2016-01-01T00:00:00", // Optional date to stop querying at
@@ -211,7 +212,11 @@ This server exposes the following REST API's:
211212
}
212213
}
213214
```
215+
216+
- **WEBSOCKET `/test`**
214217

218+
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.
219+
215220
- **GET `/metadata/:type`**
216221

217222
Returns metadata from elasticsearch related to elasalert's state. `:type` should be one of: elastalert_status, elastalert, elastalert_error, or silence. See [docs about the elastalert metadata index](https://elastalert.readthedocs.io/en/latest/elastalert_status.html).
@@ -220,6 +225,10 @@ This server exposes the following REST API's:
220225

221226
Returns field mapping from elasticsearch for a given index.
222227

228+
- **GET `/search/:index`**
229+
230+
Performs elasticsearch query on behalf of the API. JSON body to this endpoint will become body of an ES search.
231+
223232
- **[WIP] GET `/config`**
224233

225234
Gets the ElastAlert configuration from `config.yaml` in `elastalertPath` (from the config).

config/config-hisoric-data-example.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"appName": "elastalert-server",
33
"port": 3030,
4+
"wsport": 3333,
45
"elastalertPath": "/opt/elastalert",
56
"start": "2014-01-01T00:00:00",
67
"end": "2016-01-01T00:00:00",
@@ -14,5 +15,9 @@
1415
"templatesPath": {
1516
"relative": true,
1617
"path": "/rule_templates"
17-
}
18+
},
19+
"es_host": "elasticsearch",
20+
"es_port": 9200,
21+
"writeback_index": "elastalert_status"
22+
1823
}

config/config-local-elastalert-installation.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"appName": "elastalert-server",
33
"port": 3030,
4+
"wsport": 3333,
45
"elastalertPath": "/opt/elastalert",
56
"verbose": false,
67
"es_debug": false,
@@ -12,5 +13,9 @@
1213
"templatesPath": {
1314
"relative": false,
1415
"path": "/opt/elastalert/rule_templates"
15-
}
16+
},
17+
"es_host": "elasticsearch",
18+
"es_port": 9200,
19+
"writeback_index": "elastalert_status"
20+
1621
}

config/config.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"appName": "elastalert-server",
33
"port": 3030,
4+
"wsport": 3333,
45
"elastalertPath": "/opt/elastalert",
56
"verbose": false,
67
"es_debug": false,
@@ -13,7 +14,7 @@
1314
"relative": true,
1415
"path": "/rule_templates"
1516
},
16-
"es_host": "localhost",
17+
"es_host": "elasticsearch",
1718
"es_port": 9200,
1819
"writeback_index": "elastalert_status"
1920
}

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@bitsensor/elastalert",
3-
"version": "1.0.0",
3+
"version": "2.0.0",
44
"description": "A server that runs ElastAlert and exposes REST API's for manipulating rules and alerts.",
55
"license": "MIT",
66
"main": "index.js",
@@ -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/process/index.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export default class ProcessController {
99

1010
constructor() {
1111
this._elastalertPath = config.get('elastalertPath');
12+
this._onExitCallbacks = [];
1213
this._status = Status.IDLE;
1314

1415
/**
@@ -18,6 +19,10 @@ export default class ProcessController {
1819
this._process = null;
1920
}
2021

22+
onExit(onExitCallback) {
23+
this._onExitCallbacks.push(onExitCallback);
24+
}
25+
2126
get status() {
2227
return this._status;
2328
}
@@ -38,7 +43,7 @@ export default class ProcessController {
3843

3944
// Create ElastAlert index if it doesn't exist yet
4045
logger.info('Creating index');
41-
var indexCreate = spawnSync('python', ['-m', 'elastalert.create_index', '--index', 'elastalert_status', '--old-index', ''], {
46+
var indexCreate = spawnSync('python', ['-m', 'elastalert.create_index', '--index', config.get('writeback_index'), '--old-index', ''], {
4247
cwd: this._elastalertPath
4348
});
4449

@@ -112,6 +117,12 @@ export default class ProcessController {
112117
this._status = Status.ERROR;
113118
}
114119
this._process = null;
120+
121+
this._onExitCallbacks.map(function(onExitCallback) {
122+
if (onExitCallback !== null) {
123+
onExitCallback();
124+
}
125+
});
115126
});
116127

117128
// Set listener for ElastAlert error

src/controllers/test/index.js

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

22-
testRule(rule, options) {
22+
testRule(rule, options, socket) {
2323
const self = this;
2424
let tempFileName = '~' + randomstring.generate() + '.temp';
2525
let tempFilePath = path.join(self.testFolder, tempFileName);
@@ -55,16 +55,42 @@ 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+
fileSystem.deleteFile(tempFilePath)
71+
.catch(function (error) {
72+
logger.error(`Failed to delete temporary test file ${tempFilePath} with error:`, error);
73+
});
74+
});
75+
}
76+
6377
testProcess.stdout.on('data', function (data) {
78+
if (socket) {
79+
socket.send(JSON.stringify({
80+
event: 'result',
81+
data: data.toString()
82+
}));
83+
}
6484
stdoutLines.push(data.toString());
6585
});
6686

6787
testProcess.stderr.on('data', function (data) {
88+
if (socket) {
89+
socket.send(JSON.stringify({
90+
event: 'progress',
91+
data: data.toString()
92+
}));
93+
}
6894
stderrLines.push(data.toString());
6995
});
7096

@@ -77,8 +103,10 @@ export default class TestController {
77103
resolve(stdoutLines.join('\n'));
78104
}
79105
} else {
80-
reject(stderrLines.join('\n'));
81-
logger.error(stderrLines.join('\n'));
106+
if (!socket) {
107+
reject(stderrLines.join('\n'));
108+
logger.error(stderrLines.join('\n'));
109+
}
82110
}
83111

84112
fileSystem.deleteFile(tempFilePath)
@@ -95,6 +123,8 @@ export default class TestController {
95123
logger.error(`Failed to write file ${tempFileName} to ${self.testFolder} with error:`, error);
96124
reject(error);
97125
});
126+
}).catch((error) => {
127+
logger.error('Failed to test rule with error:', error);
98128
});
99129
}
100130

src/elastalert_server.js

Lines changed: 25 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';
@@ -69,6 +70,10 @@ export default class ElastalertServer {
6970
self._fileSystemController = new FileSystem();
7071
self._processController = new ProcessController();
7172
self._processController.start();
73+
self._processController.onExit(function() {
74+
// If the elastalert process exits, we should stop the server.
75+
process.exit(0);
76+
});
7277

7378
self._rulesController = new RulesController();
7479
self._templatesController = new TemplatesController();
@@ -77,8 +82,27 @@ export default class ElastalertServer {
7782
self._fileSystemController.createDirectoryIfNotExists(self.getDataFolder()).catch(function (error) {
7883
logger.error('Error creating data folder with error:', error);
7984
});
80-
85+
8186
logger.info('Server listening on port ' + config.get('port'));
87+
88+
let wss = listen(config.get('wsport'));
89+
90+
wss.on('connection', ws => {
91+
ws.on('message', (data) => {
92+
try {
93+
data = JSON.parse(data);
94+
if (data.rule) {
95+
let rule = data.rule;
96+
let options = data.options;
97+
self._testController.testRule(rule, options, ws);
98+
}
99+
} catch (error) {
100+
console.log(error);
101+
}
102+
});
103+
});
104+
105+
logger.info('Websocket listening on port 3333');
82106
} catch (error) {
83107
logger.error('Starting server failed with error:', error);
84108
process.exit(1);

src/handlers/metadata/get.js

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,43 @@ import config from '../../common/config';
22
import { getClient } from '../../common/elasticsearch_client';
33

44

5+
function escapeLuceneSyntax(str) {
6+
return [].map
7+
.call(str, char => {
8+
if (
9+
char === '/' ||
10+
char === '+' ||
11+
char === '-' ||
12+
char === '&' ||
13+
char === '|' ||
14+
char === '!' ||
15+
char === '(' ||
16+
char === ')' ||
17+
char === '{' ||
18+
char === '}' ||
19+
char === '[' ||
20+
char === ']' ||
21+
char === '^' ||
22+
char === '"' ||
23+
char === '~' ||
24+
char === '*' ||
25+
char === '?' ||
26+
char === ':' ||
27+
char === '\\'
28+
) {
29+
return `\\${char}`;
30+
}
31+
return char;
32+
})
33+
.join('');
34+
}
35+
536
function getQueryString(request) {
637
if (request.params.type === 'elastalert_error') {
738
return '*:*';
839
}
940
else {
10-
return `rule_name:${request.query.rule_name || '*'}`;
41+
return `rule_name:"${escapeLuceneSyntax(request.query.rule_name) || '*'}"`;
1142
}
1243
}
1344

src/handlers/search/get.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { getClient } from '../../common/elasticsearch_client';
2+
3+
export default function searchHandler(request, response) {
4+
/**
5+
* @type {ElastalertServer}
6+
*/
7+
var client = getClient();
8+
9+
client.search({
10+
index: request.params.index,
11+
body: request.body
12+
}).then(function(resp) {
13+
response.send(resp);
14+
}, function(error) {
15+
response.send({ error });
16+
});
17+
18+
}

0 commit comments

Comments
 (0)