/* * common.js: Common utility functions for requesting against Loggly APIs * * (C) 2010 Charlie Robbins * MIT LICENSE * */ // // Variables for Bulk // var arrSize = 100, arrMsg = [], timerFunction = null, sendBulkInMs = 5000; // // Variables for buffer array // var arrBufferedMsg = [], timerFunctionForBufferedLogs = null; // // flag variable to validate authToken // var isValidToken = true; // // attach event id with each event in both bulk and input mode // var bulkId = 1, inputId = 1; // // Variables for error retry // var numberOfRetries = 5, eventRetried = 2, sleepTimeMs, responseCode; // // Object to hold status codes // var httpStatusCode = { badToken: { message: 'Forbidden', code: 403 }, success: { message: 'Success', code: 200 } } var request = require('request'); var common = exports; // // Core method that actually sends requests to Loggly. // This method is designed to be flexible w.r.t. arguments // and continuation passing given the wide range of different // requests required to fully implement the Loggly API. // // Continuations: // 1. 'callback': The callback passed into every node-loggly method // 2. 'success': A callback that will only be called on successful requests. // This is used throughout node-loggly to conditionally // do post-request processing such as JSON parsing. // // Possible Arguments (1 & 2 are equivalent): // 1. common.loggly('some-fully-qualified-url', auth, callback, success) // 2. common.loggly('GET', 'some-fully-qualified-url', auth, callback, success) // 3. common.loggly('DELETE', 'some-fully-qualified-url', auth, callback, success) // 4. common.loggly({ method: 'POST', uri: 'some-url', body: { some: 'body'} }, callback, success) // common.loggly = function () { var args = Array.prototype.slice.call(arguments), success = args.pop(), callback = args.pop(), responded, requestBody, headers, method, auth, proxy, isBulk, uri, bufferOptions, networkErrorsOnConsole; // // Now that we've popped off the two callbacks // We can make decisions about other arguments // if (args.length === 1) { if (typeof args[0] === 'string') { // // If we got a string assume that it's the URI // method = 'GET'; uri = args[0]; } else { method = args[0].method || 'GET'; uri = args[0].uri; requestBody = args[0].body; auth = args[0].auth; isBulk = args[0].isBulk; headers = args[0].headers; proxy = args[0].proxy; bufferOptions = args[0].bufferOptions; networkErrorsOnConsole = args[0].networkErrorsOnConsole; } } else if (args.length === 2) { method = 'GET'; uri = args[0]; auth = args[1]; } else { method = args[0]; uri = args[1]; auth = args[2]; } function onError(err) { if(!isValidToken){ // eslint-disable-next-line no-undef console.log(err); return; } var arrayLogs = []; if(isBulk) { arrayLogs = requestOptions.body.split('\n'); } else { arrayLogs.push(requestOptions.body); } storeLogs(arrayLogs); if (!responded) { responded = true; if (callback) { callback(err) } } } var requestOptions = { uri: (isBulk && headers['X-LOGGLY-TAG']) ? uri + '/tag/' + headers['X-LOGGLY-TAG'] : uri, method: method, headers: isBulk ? {} : headers || {}, // Set headers empty for bulk proxy: proxy }; var requestOptionsForBufferedLogs = JSON.parse(JSON.stringify(requestOptions)) if (auth) { // eslint-disable-next-line no-undef requestOptions.headers.authorization = 'Basic ' + Buffer.from(auth.username + ':' + auth.password, 'base64'); } function popMsgsAndSend() { if (isBulk) { var bulk = createBulk(arrMsg); sendBulkLogs(bulk); } else { var input = createInput(requestBody); sendInputLogs(input); } } // eslint-disable-next-line no-unused-vars function createBulk(msgs) { var bulkMsg = {}; bulkMsg.msgs = arrMsg.slice(); bulkMsg.attemptNumber = 1; bulkMsg.sleepUntilNextRetry = 2 * 1000; bulkMsg.id = bulkId++; return bulkMsg; } // eslint-disable-next-line no-unused-vars function createInput(msgs) { var inputMsg = {}; inputMsg.msgs = requestBody; requestOptions.body = requestBody; inputMsg.attemptNumber = 1; inputMsg.sleepUntilNextRetry = 2 * 1000; inputMsg.id = inputId++; return inputMsg; } function sendInputLogs(input) { try { request(requestOptions, function (err, res, body) { if (err) { // In rare cases server is busy if (err.code === 'ETIMEDOUT' || err.code === 'ECONNRESET' || err.code === 'ESOCKETTIMEDOUT' || err.code === 'ECONNABORTED') { retryOnError(input, err); } else { return onError(err); } } else { responseCode = res.statusCode; if (responseCode === httpStatusCode.badToken.code) { isValidToken = false; return onError((new Error('Loggly Error (' + responseCode + '): ' + httpStatusCode.badToken.message))); } if (responseCode === httpStatusCode.success.code && input.attemptNumber >= eventRetried) { // eslint-disable-next-line no-undef if (networkErrorsOnConsole) console.log('log #' + input.id + ' sent successfully after ' + input.attemptNumber + ' retries'); } if (responseCode === httpStatusCode.success.code) { success(res, body); } else { retryOnError(input, res); } } }); } catch (ex) { onError(ex); } } function sendBulkLogs(bulk) { // // Join Array Message with new line ('\n') character // if (arrMsg.length) { requestOptions.body = arrMsg.join('\n'); arrMsg.length = 0; } try { request(requestOptions, function (err, res, body) { if (err) { // In rare cases server is busy if (err.code === 'ETIMEDOUT' || err.code === 'ECONNRESET' || err.code === 'ESOCKETTIMEDOUT' || err.code === 'ECONNABORTED') { retryOnError(bulk, err); } else { return onError(err); } } else { responseCode = res.statusCode; if (responseCode === httpStatusCode.badToken.code) { isValidToken = false; return onError((new Error('Loggly Error (' + responseCode + '): ' + httpStatusCode.badToken.message))); } if (responseCode === httpStatusCode.success.code && bulk.attemptNumber >= eventRetried) { // eslint-disable-next-line no-undef if (networkErrorsOnConsole) console.log('log #' + bulk.id + ' sent successfully after ' + bulk.attemptNumber + ' retries'); } if (responseCode === httpStatusCode.success.code) { success(res, body); } else { retryOnError(bulk, res); } } }); } catch (ex) { onError(ex); } } if (isBulk && isValidToken) { if (timerFunction === null) { // eslint-disable-next-line no-undef timerFunction = setInterval(function () { if (arrMsg.length) popMsgsAndSend(); if (timerFunction && !arrMsg.length) { // eslint-disable-next-line no-undef clearInterval(timerFunction) timerFunction = null; } }, sendBulkInMs); } if (Array.isArray(requestBody)) { arrMsg.push.apply(arrMsg, requestBody); } else { arrMsg.push(requestBody); } if (arrMsg.length === arrSize) { popMsgsAndSend(); } } else if(isValidToken) { if (requestBody) { popMsgsAndSend(); } } // //function to retry sending logs maximum 5 times if any error occurs // function retryOnError(mode, response) { function tryAgainIn(sleepTimeMs) { // eslint-disable-next-line no-undef if (networkErrorsOnConsole) console.log('log #' + mode.id + ' - Trying again in ' + sleepTimeMs + '[ms], attempt no. ' + mode.attemptNumber); // eslint-disable-next-line no-undef setTimeout(function () { isBulk ? sendBulkLogs(mode) : sendInputLogs(mode); }, sleepTimeMs); } if (mode.attemptNumber >= numberOfRetries) { if (response.code) { // eslint-disable-next-line no-undef if (networkErrorsOnConsole) console.error('Failed log #' + mode.id + ' after ' + mode.attemptNumber + ' retries on error = ' + response, response); } else { // eslint-disable-next-line no-undef if (networkErrorsOnConsole) console.error('Failed log #' + mode.id + ' after ' + mode.attemptNumber + ' retries on error = ' + response.statusCode + ' ' + response.statusMessage); } } else { if (response.code) { // eslint-disable-next-line no-undef if (networkErrorsOnConsole) console.log('log #' + mode.id + ' - failed on error: ' + response); } else { // eslint-disable-next-line no-undef if (networkErrorsOnConsole) console.log('log #' + mode.id + ' - failed on error: ' + response.statusCode + ' ' + response.statusMessage); } sleepTimeMs = mode.sleepUntilNextRetry; mode.sleepUntilNextRetry = mode.sleepUntilNextRetry * 2; mode.attemptNumber++; tryAgainIn(sleepTimeMs) } } // // retries to send buffered logs to loggly in every 30 seconds // if (timerFunctionForBufferedLogs === null && bufferOptions) { // eslint-disable-next-line no-undef timerFunctionForBufferedLogs = setInterval(function () { if (arrBufferedMsg.length) sendBufferdLogstoLoggly(); if (timerFunctionForBufferedLogs && !arrBufferedMsg.length) { // eslint-disable-next-line no-undef clearInterval(timerFunctionForBufferedLogs); timerFunctionForBufferedLogs = null; } }, bufferOptions.retriesInMilliSeconds); } function sendBufferdLogstoLoggly() { if (!arrBufferedMsg.length) return; var arrayMessage = []; var bulkModeBunch = arrSize; var inputModeBunch = 1; var logsInBunch = isBulk ? bulkModeBunch : inputModeBunch; arrayMessage = arrBufferedMsg.slice(0, logsInBunch); requestOptionsForBufferedLogs.body = isBulk ? arrayMessage.join('\n') : arrayMessage[0]; // eslint-disable-next-line no-unused-vars request(requestOptionsForBufferedLogs, function (err, res, body) { if(err) return; // eslint-disable-next-line no-undef statusCode = res.statusCode; // eslint-disable-next-line no-undef if(statusCode === httpStatusCode.success.code) { arrBufferedMsg.splice(0, logsInBunch); sendBufferdLogstoLoggly(); } }); requestOptionsForBufferedLogs.body = ''; } // // This function will store logs into buffer // function storeLogs(logs) { if (!logs.length || !bufferOptions) return; var numberOfLogsToBeRemoved = (arrBufferedMsg.length + logs.length) - bufferOptions.size; if (numberOfLogsToBeRemoved > 0) arrBufferedMsg = arrBufferedMsg.splice(numberOfLogsToBeRemoved); arrBufferedMsg = arrBufferedMsg.concat(logs); } }; // // ### function serialize (obj, key) // #### @obj {Object|literal} Object to serialize // #### @key {string} **Optional** Optional key represented by obj in a larger object // Performs simple comma-separated, `key=value` serialization for Loggly when // logging for non-JSON values. // common.serialize = function (obj, key) { if (obj === null) { obj = 'null'; } else if (obj === undefined) { obj = 'undefined'; } else if (obj === false) { obj = 'false'; } if (typeof obj !== 'object') { return key ? key + '=' + obj : obj; } var msg = '', keys = Object.keys(obj), length = keys.length; for (var i = 0; i < length; i++) { if (Array.isArray(obj[keys[i]])) { msg += keys[i] + '=['; for (var j = 0, l = obj[keys[i]].length; j < l; j++) { msg += common.serialize(obj[keys[i]][j]); if (j < l - 1) { msg += ', '; } } msg += ']'; } else { msg += common.serialize(obj[keys[i]], keys[i]); } if (i < length - 1) { msg += ', '; } } return msg; }; // // function clone (obj) // Helper method for deep cloning pure JSON objects // i.e. JSON objects that are either literals or objects (no Arrays, etc) // common.clone = function (obj) { var clone = {}; for (var i in obj) { clone[i] = obj[i] instanceof Object ? common.clone(obj[i]) : obj[i]; } return clone; };