-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcron.js
347 lines (302 loc) · 10.6 KB
/
cron.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
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
const ACCOUNT_ID = "0" // You account ID
const REGION = "US" // US or EU
const BLOCKSIZE = 10 //minutes - you shold ensure you monitor is executed at least once within this time period.
const NAMESPACE="cronsynth" //Adjust this if you're running multiple copies of this monitor and dont want them to clash. Each monitor should use unique value.
//Add your jobs to this array
const JOBS = [
{
name: "Job 1",
cron: {
months: [3], // 1 (Jan) 2 ... 11 12 (Dec)
dayOfMonth: [], // 1 2 ... 30 31
dayOfWeek: [], // 1 (Monday) 2 ... 6 7 (Sunday)
hourOfDay: [], // 0 1 ... 22 23
minuteOfHour: [10,30] //the values here must be multiples of BLOCKSIZE!
},
fn: async ()=>{
await sleep(5000);
console.log("Job 1 ran!")
}
},
{
name: "Job 2",
cron: {
months: [], //all empty means it will execute on every run!
dayOfMonth: [],
dayOfWeek: [],
hourOfDay: [],
minuteOfHour: []
},
fn: async ()=>{
await sleep(5000);
console.log("Job 2 ran!")
}
}
]
/*
You shouldnt need to configure anything below here!
*/
const CRON_SYNTH_VERSION="1.0.1"
const DEFAULT_TIMEOUT = 10000 //timeout on http requests
let RUNNING_LOCALLY=false
/*
* ========== LOCAL TESTING CONFIGURATION ===========================
* This is used if running from local laptop rather than a minion
*/
const IS_LOCAL_ENV = typeof $http === 'undefined';
if (IS_LOCAL_ENV) {
RUNNING_LOCALLY=true
var $http = require("request");
var $secure = {}
var $env = {}
$env.MONITOR_ID="local"
$env.JOB_ID="0"
//When testing ONLY set you API keys here
$secure.CS_INSERT_KEY = "XXXNRAL" ///...NRAL
$secure.CS_QUERY_KEY = "NRAK-XXX" //NRAK...
console.log("Running in local mode")
}
let moment = require('moment');
let assert = require('assert');
let INSERT_KEY = $secure.CS_INSERT_KEY
let QUERY_KEY = $secure.CS_QUERY_KEY
const GRAPHQL_URL= REGION=="US" ? "https://api.newrelic.com/graphql" : "https://api.eu.newrelic.com/graphql"
const METRIC_API_URL = REGION=="US" ? "https://metric-api.newrelic.com/metric/v1" : "https://metric-api.eu.newrelic.com/metric/v1"
/*
* ========== SOME HELPER FUNCTIONS ===========================
*/
/*
* asyncForEach()
*
* A handy version of forEach that supports await.
* @param {Object[]} array - An array of things to iterate over
* @param {function} callback - The callback for each item
*/
async function asyncForEach(array, callback) {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array);
}
}
/*
* isObject()
*
* A handy check for if a var is an object
*/
function isObject(val) {
if (val === null) { return false;}
return ( (typeof val === 'function') || (typeof val === 'object') );
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/*
* genericServiceCall()
* Generic service call helper for commonly repeated tasks
*
* @param {number} responseCodes - The response code (or array of codes) expected from the api call (e.g. 200 or [200,201])
* @param {Object} options - The standard http request options object
* @param {function} success - Call back function to run on successfule request
*/
const genericServiceCall = function(responseCodes,options,success) {
!('timeout' in options) && (options.timeout = DEFAULT_TIMEOUT) //add a timeout if not already specified
let possibleResponseCodes=responseCodes
if(typeof(responseCodes) == 'number') { //convert to array if not supplied as array
possibleResponseCodes=[responseCodes]
}
return new Promise((resolve, reject) => {
$http(options, function callback(error, response, body) {
if(error) {
reject(`Connection error on url '${options.url}'`)
} else {
if(!possibleResponseCodes.includes(response.statusCode)) {
let errmsg=`Expected [${possibleResponseCodes}] response code but got '${response.statusCode}' from url '${options.url}'`
reject(errmsg)
} else {
resolve(success(body,response,error))
}
}
});
})
}
/*
* setAttribute()
* Sets a custom attribute on the synthetic record
*
* @param {string} key - the key name
* @param {Strin|Object} value - the value to set
*/
const setAttribute = function(key,value) {
if(!RUNNING_LOCALLY) { //these only make sense when running on a minion
$util.insights.set(key,value)
} else {
console.log(`Set attribute '${key}' to ${value}`)
}
}
/*
* sendDataToNewRelic()
* Sends a metrics payload to New Relic
*
* @param {object} data - the payload to send
*/
const sendDataToNewRelic = async (data) => {
let request = {
url: METRIC_API_URL,
method: 'POST',
headers :{
"Api-Key": INSERT_KEY
},
body: JSON.stringify(data)
}
console.log("\nSending data to NR metrics API...")
return genericServiceCall([200,202],request,(body,response,error)=>{
if(error) {
log(`NR Post failed : ${error} `,true)
return false
} else {
return true
}
})
}
const checkRecentRun = async (timeBlock) => {
const graphQLQuery=`{
actor {
account(id: ${ACCOUNT_ID}) {
nrql(query: "select count(*) from Metric where ${NAMESPACE}.timeBlock='${timeBlock}' since 1 hour ago") {
results
}
}
}
}
`
const options = {
url: GRAPHQL_URL,
method: 'POST',
headers :{
"Content-Type": "application/json",
"API-Key": QUERY_KEY
},
body: JSON.stringify({ "query": graphQLQuery})
}
let body = await genericServiceCall([200],options,(body)=>{return body})
try {
let bodyJSON
if(isObject(body)) {
bodyJSON = body
} else {
bodyJSON = JSON.parse(body)
}
let recentRuns=bodyJSON.data.actor.account.nrql.results[0].count
if(!recentRuns) {
console.log(`Recent run for this timeblock (${timeBlock}) not detected`)
return false
} else {
console.log(`${recentRuns} Recent runs were detected for this time block (${timeBlock})`)
return true
}
} catch(e) {
console.log("Error: Response from New Relic could not be parsed",e)
assert.fail(e)
}
}
//creates a simple timeblock reference HH:m rounded to the blocksize
const genTimeBlock = (time) => {
const timeBlock = time.format("HH") + ":" + Math.floor(parseInt(time.format("m")) / BLOCKSIZE) * BLOCKSIZE
console.log(`This ${BLOCKSIZE}min time block: ${timeBlock}`)
return timeBlock
}
//Records the script running in NRDB
const recordRun = async (timeBlock,numJobs) => {
let commonMetricBlock={"attributes": {}}
commonMetricBlock.attributes[`${NAMESPACE}.timeBlock`]=timeBlock
commonMetricBlock.attributes[`${NAMESPACE}.monitorId`]=$env.MONITOR_ID
commonMetricBlock.attributes[`${NAMESPACE}.jobId`]=$env.JOB_ID
commonMetricBlock.attributes[`cronsynth.version`]=CRON_SYNTH_VERSION
let metricsPayLoad=[{
"common" : commonMetricBlock,
"metrics": [{
name: `${NAMESPACE}.value`,
type: "gauge",
value: numJobs,
timestamp: Math.round(Date.now()/1000)
}]
}]
console.log(`Logging ${numJobs} jobs run`)
let NRPostStatus = await sendDataToNewRelic(metricsPayLoad)
if( NRPostStatus === true ){
setAttribute("nrPostStatus","success")
console.log("NR Post successful")
} else {
setAttribute("nrPostStatus","failed")
console.log("NR Post failed")
}
}
//runs the jobs nased on their settings
async function jobRunner(timeNow) {
let jobsTriggered=0
const month = parseInt(timeNow.format("M")) //1 2 ... 11 12
const dayOfMonth = parseInt(timeNow.format("D")) //1 2 ... 30 31
const dayOfWeek = parseInt(timeNow.format("E")) //1 (mon) 2 ... 6 7 (sun)
const hourOfDay = parseInt(timeNow.format("H")) //0 1 ... 22 23
const minuteOfHour = parseInt(Math.floor(parseInt(timeNow.format("m")) / BLOCKSIZE) * BLOCKSIZE)
console.log(`Now: month: ${month}, dayOfMonth ${dayOfMonth}, dayOfWeek:${dayOfWeek}, hourOfDay:${hourOfDay}, minuteOfDay:${minuteOfHour}`)
await asyncForEach(JOBS,async (job)=>{
if(job.cron) {
let executeJob = true
//months
if(job.cron.months && job.cron.months.length > 0 ) {
executeJob = job.cron.months.includes(month)
}
//dayOfMonth
if(job.cron.dayOfMonth && job.cron.dayOfMonth.length > 0 ) {
executeJob = job.cron.dayOfMonth.includes(dayOfMonth)
}
//dayOfWeek
if(job.cron.dayOfWeek && job.cron.dayOfWeek.length > 0 ) {
executeJob = job.cron.dayOfWeek.includes(dayOfWeek)
}
//hourOfDay
if(job.cron.hourOfDay && job.cron.hourOfDay.length > 0 ) {
executeJob = job.cron.hourOfDay.includes(hourOfDay)
}
//minuteOfHour
if(job.cron.minuteOfHour && job.cron.minuteOfHour.length > 0 ) {
executeJob = job.cron.minuteOfHour.includes(minuteOfHour)
}
if(executeJob){
jobsTriggered++
console.log(`Execute job: ${job.name}`)
await job.fn()
console.log(`Finished job: ${job.name} `)
}
}
})
return jobsTriggered
}
async function cronRunner() {
let timeNow = moment().utc()
let timeBlock=genTimeBlock(timeNow)
const recentRuns = await checkRecentRun(timeBlock)
if(recentRuns) {
console.log("This timeblock has run already, skipping jobs")
setAttribute("jobsSkipped","Yes")
setAttribute("jobsTriggered",0)
} else {
setAttribute("jobsSkipped","No")
const jobsTriggered = await jobRunner(timeNow)
setAttribute("jobsTriggered",jobsTriggered)
await recordRun(timeBlock,jobsTriggered)
}
return true
}
try {
cronRunner()
.then((success)=>{
if(success === true ) {
console.log("Completed successfully")
} else {
console.log("Completed with errors")
}
})
} catch(e) {
console.log("Unexpected errors: ",e)
}