-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathworkers.js
270 lines (234 loc) · 9.85 KB
/
workers.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
/*
* Worker-related tasks
*
* @author Faiz A. Farooqui <faiz@geekyants.com>
*/
// Dependencies
const path = require('path');
const fs = require('fs');
const https = require('https');
const http = require('http');
const url = require('url');
const util = require('util');
const debug = util.debuglog('workers');
const _data = require('./data');
const helpers = require('./helpers');
const _logs = require('./logs');
// Instantiate the worker module object
var workers = {};
// Lookup all checks, get their data, send to validator
workers.gatherAllChecks = () => {
// Get all the checks
_data.list('checks', (err, checks) => {
if (!err && checks && checks.length > 0) {
checks.forEach((check) => {
// Read in the check data
_data.read('checks', check, (err, originalCheckData) => {
if (!err && originalCheckData) {
// Pass it to the check validator, and let that function continue the function or log the error(s) as needed
workers.validateCheckData(originalCheckData);
} else {
debug('Error reading one of the check\'s data: ', err);
}
});
});
} else {
debug('Error Could not find any checks to process');
}
});
};
// Sanity-check the check-data,
workers.validateCheckData = (originalCheckData) => {
originalCheckData = typeof(originalCheckData) == 'object' && originalCheckData !== null ? originalCheckData : {};
originalCheckData.id = typeof(originalCheckData.id) == 'string' && originalCheckData.id.trim().length == 20 ? originalCheckData.id.trim() : false;
originalCheckData.userPhone = typeof(originalCheckData.userPhone) == 'string' && originalCheckData.userPhone.trim().length == 10 ? originalCheckData.userPhone.trim() : false;
originalCheckData.protocol = typeof(originalCheckData.protocol) == 'string' && ['http', 'https'].indexOf(originalCheckData.protocol) > -1 ? originalCheckData.protocol : false;
originalCheckData.url = typeof(originalCheckData.url) == 'string' && originalCheckData.url.trim().length > 0 ? originalCheckData.url.trim() : false;
originalCheckData.method = typeof(originalCheckData.method) == 'string' && ['post', 'get', 'put', 'delete'].indexOf(originalCheckData.method) > -1 ? originalCheckData.method : false;
originalCheckData.successCodes = typeof(originalCheckData.successCodes) == 'object' && originalCheckData.successCodes instanceof Array && originalCheckData.successCodes.length > 0 ? originalCheckData.successCodes : false;
originalCheckData.timeoutSeconds = typeof(originalCheckData.timeoutSeconds) == 'number' && originalCheckData.timeoutSeconds % 1 === 0 && originalCheckData.timeoutSeconds >= 1 && originalCheckData.timeoutSeconds <= 5 ? originalCheckData.timeoutSeconds : false;
// Set the keys that may not be set (if the workers have never seen this check before)
originalCheckData.state = typeof(originalCheckData.state) == 'string' && ['up', 'down'].indexOf(originalCheckData.state) > -1 ? originalCheckData.state : 'down';
originalCheckData.lastChecked = typeof(originalCheckData.lastChecked) == 'number' && originalCheckData.lastChecked > 0 ? originalCheckData.lastChecked : false;
// If all checks pass, pass the data along to the next step in the process
if (originalCheckData.id && originalCheckData.userPhone && originalCheckData.protocol && originalCheckData.url
&& originalCheckData.method && originalCheckData.successCodes && originalCheckData.timeoutSeconds) {
workers.performCheck(originalCheckData);
} else {
// If checks fail, log the error and fail silently
debug('Error: one of the checks is not properly formatted. Skipping.');
}
};
// Perform the check, send the originalCheck data and the outcome of the check process to the next step in the process
workers.performCheck = (originalCheckData) => {
// Prepare the intial check outcome
var checkOutcome = {
error: false,
responseCode: false
};
// Mark that the outcome has not been sent yet
var outcomeSent = false;
// Parse the hostname and path out of the originalCheckData
var parsedUrl = url.parse(originalCheckData.protocol+'://'+originalCheckData.url, true);
var hostName = parsedUrl.hostname;
var path = parsedUrl.path; // Using path not pathname because we want the query string
// Construct the request
var requestDetails = {
protocol: originalCheckData.protocol+':',
hostname: hostName,
method: originalCheckData.method.toUpperCase(),
path: path,
timeout: originalCheckData.timeoutSeconds * 1000
};
// Instantiate the request object (using either the http or https module)
var _moduleToUse = originalCheckData.protocol == 'http' ? http : https;
var req = _moduleToUse.request(requestDetails, (res) => {
// Grab the status of the sent request
var status = res.statusCode;
// Update the checkOutcome and pass the data along
checkOutcome.responseCode = status;
if (! outcomeSent) {
workers.processCheckOutcome(originalCheckData, checkOutcome);
outcomeSent = true;
}
});
// Bind to the error event so it doesn't get thrown
req.on('error', (e) => {
// Update the checkOutcome and pass the data along
checkOutcome.error = {error: true, value: e};
if (! outcomeSent) {
workers.processCheckOutcome(originalCheckData, checkOutcome);
outcomeSent = true;
}
});
// Bind to the timeout event
req.on('timeout', () => {
// Update the checkOutcome and pass the data along
checkOutcome.error = {error: true, value: 'timeout'};
if (! outcomeSent) {
workers.processCheckOutcome(originalCheckData, checkOutcome);
outcomeSent = true;
}
});
// End the request
req.end();
};
// Process the check outcome, update the check data as needed, trigger an alert if needed
// Special logic for accomodating a check that has never been tested before (don't alert on that one)
workers.processCheckOutcome = (originalCheckData, checkOutcome) => {
// Decide if the check is considered up or down
var state = !checkOutcome.error && checkOutcome.responseCode && originalCheckData.successCodes.indexOf(checkOutcome.responseCode) > -1 ? 'up' : 'down';
// Decide if an alert is warranted
var alertWarranted = originalCheckData.lastChecked && originalCheckData.state !== state ? true : false;
// Log the outcome
var timeOfCheck = Date.now();
workers.log(originalCheckData,checkOutcome,state,alertWarranted,timeOfCheck);
// Update the check data
var newCheckData = originalCheckData;
newCheckData.state = state;
newCheckData.lastChecked = timeOfCheck;
// Save the updates
_data.update('checks', newCheckData.id, newCheckData, (err) => {
if (! err) {
// Send the new check data to the next phase in the process if needed
if (alertWarranted) {
workers.alertUserToStatusChange(newCheckData);
} else {
debug('Check outcome has not changed, no alert needed');
}
} else {
debug('Error trying to save updates to one of the checks');
}
});
};
// Alert the user as to a change in their check status
workers.alertUserToStatusChange = (newCheckData) => {
var msg = 'Alert: Your check for '+newCheckData.method.toUpperCase()+' '+newCheckData.protocol+'://'+newCheckData.url+' is currently '+newCheckData.state;
helpers.sendTwilioSms(newCheckData.userPhone, msg, (err) => {
if (! err){
debug('Success: User was alerted to a status change in their check, via sms: ', msg);
} else {
debug('Error: Could not send sms alert to user who had a state change in their check: ', err);
}
});
};
// Send check data to a log file
workers.log = (originalCheckData, checkOutcome, state, alertWarranted, timeOfCheck) => {
// Form the log data
var logData = {
check: originalCheckData,
outcome: checkOutcome,
state: state,
alert: alertWarranted,
time: timeOfCheck
};
// Convert the data to a string
var logString = JSON.stringify(logData);
// Determine the name of the log file
var logFileName = originalCheckData.id;
// Append the log string to the file
_logs.append(logFileName, logString, (err) => {
if (!err){
debug('Logging to file succeeded');
} else {
debug('Logging to file failed');
}
});
};
// Timer to execute the worker-process once per minute
workers.loop = () => {
setInterval(() => {
workers.gatherAllChecks();
}, 1000 * 60);
};
// Rotate (compress) the log files
workers.rotateLogs = () => {
// List all the (non compressed) log files
_logs.list(false, (err, logs) => {
if (!err && logs && logs.length > 0) {
logs.forEach((logName) => {
// Compress the data to a different file
var logId = logName.replace('.log','');
var newFileId = logId+'-'+Date.now();
_logs.compress(logId, newFileId, (err) => {
if (! err) {
// Truncate the log
_logs.truncate(logId, (err) => {
if (! err) {
debug('Success truncating logfile');
} else {
debug('Error truncating logfile');
}
});
} else {
debug('Error compressing one of the log files.', err);
}
});
});
} else {
debug('Error: Could not find any logs to rotate');
}
});
};
// Timer to execute the log-rotation process once per day
workers.logRotationLoop = () => {
setInterval(() => {
workers.rotateLogs();
},1000 * 60 * 60 * 24);
}
// Init script
workers.init = () => {
// Send to console, in yellow.
// [Note: '%s' is replaced by the 2ns parameter!]
console.log('\x1b[33m%s\x1b[0m', 'Background workers are running...');
// Execute all the checks immediately
workers.gatherAllChecks();
// Call the loop so the checks will execute later on
workers.loop();
// Compress all the logs immediately
workers.rotateLogs();
// Call the compression loop so checks will execute later on
workers.logRotationLoop();
};
// Export the module
module.exports = workers;