diff --git a/README.md b/README.md index c4eba7c..5ddfb64 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ See the `examples` folder for more examples: * `basic.js`: shows how to configure a logger and send a log message to Splunk. * `batching.js`: shows how to queue log messages, and send them in batches. * `middleware.js`: shows how to add an express-like middleware function to be called before sending log messages to Splunk. +* `retry.js`: shows how to configure retries on errors. ### Basic example diff --git a/examples/all_batching.js b/examples/all_batching.js new file mode 100644 index 0000000..14865aa --- /dev/null +++ b/examples/all_batching.js @@ -0,0 +1,95 @@ +/* + * Copyright 2015 Splunk, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"): you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/** + * This example shows how to batch events with the + * SplunkLogger with all available settings: + * batchInterval, maxBatchCount, & maxBatchSize. + */ + +// Change to require("splunk-logging").Logger; +var SplunkLogger = require("../index").Logger; + +/** + * Only the token property is required. + * + * Here, batchInterval is set to flush every 1 second, when + * 10 events are queued, or when the size of queued events totals + * more than 1kb. + */ +var config = { + token: "your-token", + url: "https://localhost:8088", + batchInterval: 1000, + maxBatchCount: 10, + maxBatchSize: 1024 // 1kb +}; + +// Create a new logger +var Logger = new SplunkLogger(config); + +Logger.error = function(err, context) { + // Handle errors here + console.log("error", err, "context", context); +}; + +// Define the payload to send to Splunk's Event Collector +var payload = { + // Message can be anything, doesn't have to be an object + message: { + temperature: "70F", + chickenCount: 500 + }, + // Metadata is optional + metadata: { + source: "chicken coop", + sourcetype: "httpevent", + index: "main", + host: "farm.local", + }, + // Severity is also optional + severity: "info" +}; + +console.log("Queuing payload", payload); +// Don't need a callback here +Logger.send(payload); + +var payload2 = { + message: { + temperature: "75F", + chickenCount: 600, + note: "New chickens have arrived" + }, + metadata: payload.metadata +}; + +console.log("Queuing second payload", payload2); +// Don't need a callback here +Logger.send(payload2); + +/** + * Since we've configured batching, we don't need + * to do anything at this point. Events will + * will be sent to Splunk automatically based + * on the batching settings above. + */ + +// Kill the process +setTimeout(function() { + console.log("Events should be in Splunk! Exiting..."); + process.exit(); +}, 2000); \ No newline at end of file diff --git a/examples/basic.js b/examples/basic.js index 668c677..ca82cd0 100644 --- a/examples/basic.js +++ b/examples/basic.js @@ -23,20 +23,10 @@ var SplunkLogger = require("../index").Logger; /** * Only the token property is required. - * Defaults are listed explicitly. - * - * Alternatively, specify config.url like so: - * - * "https://localhost:8088/services/collector/event/1.0" */ var config = { token: "your-token-here", - host: "localhost", - path: "/services/collector/event/1.0", - protocol: "https", - port: 8088, - level: "info", - autoFlush: true + url: "https://localhost:8088" }; // Create a new logger @@ -49,7 +39,7 @@ Logger.error = function(err, context) { // Define the payload to send to Splunk's Event Collector var payload = { - // Message can be anything, doesn't have to be an object + // Message can be anything, it doesn't have to be an object message: { temperature: "70F", chickenCount: 500 @@ -59,13 +49,39 @@ var payload = { source: "chicken coop", sourcetype: "httpevent", index: "main", - host: "farm.local", + host: "farm.local" }, // Severity is also optional severity: "info" }; console.log("Sending payload", payload); + +/** + * Since maxBatchCount is set to 1 by default, + * calling send will immediately send the payload. + * + * The underlying HTTP POST request is made to + * + * https://localhost:8088/services/collector/event/1.0 + * + * with the following body + * + * { + * "source": "chicken coop", + * "sourcetype": "httpevent", + * "index": "main", + * "host": "farm.local", + * "event": { + * "message": { + * "temperature": "70F", + * "chickenCount": 500 + * }, + * "severity": "info" + * } + * } + * + */ Logger.send(payload, function(err, resp, body) { // If successful, body will be { text: 'Success', code: 0 } console.log("Response from Splunk", body); diff --git a/examples/custom_format.js b/examples/custom_format.js new file mode 100644 index 0000000..48266d7 --- /dev/null +++ b/examples/custom_format.js @@ -0,0 +1,114 @@ +/* + * Copyright 2015 Splunk, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"): you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/** + * This example shows how to use a custom event format for SplunkLogger. + */ + +// Change to require("splunk-logging").Logger; +var SplunkLogger = require("../index").Logger; + +/** + * Only the token property is required. + */ +var config = { + token: "your-token-here", + url: "https://localhost:8088", + maxBatchCount: 1 // Send events 1 at a time +}; + +// Create a new logger +var Logger = new SplunkLogger(config); + +Logger.error = function(err, context) { + // Handle errors here + console.log("error", err, "context", context); +}; + +/** + * Override the default eventFormatter() function, + * which takes a message and severity, returning + * any type - string or object are recommended. + * + * The message parameter can be any type. It will + * be whatever was passed to Logger.send(). + * Severity will always be a string. + * + * In this example, we're building up a string + * of key=value pairs if message is an object, + * otherwise the message value is as value for + * the message key. + * This string is prefixed with the event + * severity in square brackets. + */ +Logger.eventFormatter = function(message, severity) { + var event = "[" + severity + "]"; + + if (typeof message === "object") { + for (var key in message) { + event += key + "=" + message[key] + " "; + } + } + else { + event += "message=" + message; + } + + return event; +}; + +// Define the payload to send to Splunk's Event Collector +var payload = { + // Message can be anything, it doesn't have to be an object + message: { + temperature: "70F", + chickenCount: 500 + }, + // Metadata is optional + metadata: { + source: "chicken coop", + sourcetype: "httpevent", + index: "main", + host: "farm.local" + }, + // Severity is also optional + severity: "info" +}; + +console.log("Sending payload", payload); + +/** + * Since maxBatchCount is set to 1 by default, + * calling send will immediately send the payload. + * + * The underlying HTTP POST request is made to + * + * https://localhost:8088/services/collector/event/1.0 + * + * with the following body + * + * { + * "source": "chicken coop", + * "sourcetype": "httpevent", + * "index": "main", + * "host": "farm.local", + * "event": "[info]temperature=70F chickenCount=500 " + * } + * + */ +Logger.send(payload, function(err, resp, body) { + // If successful, body will be { text: 'Success', code: 0 } + console.log("Response from Splunk", body); +}); \ No newline at end of file diff --git a/examples/batching.js b/examples/manual_batching.js similarity index 83% rename from examples/batching.js rename to examples/manual_batching.js index b5933ae..3720c36 100644 --- a/examples/batching.js +++ b/examples/manual_batching.js @@ -18,11 +18,7 @@ * This example shows how to batch events with the * SplunkLogger by manually calling flush. * - * By default autoFlush is enabled, this means - * an HTTP request is made each time send() - * is called. - * - * By disabling autoFlush, events will be queued + * By setting maxbatchCount=0, events will be queued * until flush() is called. */ @@ -32,16 +28,12 @@ var SplunkLogger = require("../index").Logger; /** * Only the token property is required. * - * Here, autoFlush is set to false + * Here, maxBatchCount is set to 0. */ var config = { token: "your-token-here", - host: "localhost", - path: "/services/collector/event/1.0", - protocol: "https", - port: 8088, - level: "info", - autoFlush: false + url: "https://localhost:8088", + maxBatchCount: 0 // Manually flush events }; // Create a new logger @@ -64,7 +56,7 @@ var payload = { source: "chicken coop", sourcetype: "httpevent", index: "main", - host: "farm.local", + host: "farm.local" }, // Severity is also optional severity: "info" @@ -88,7 +80,7 @@ console.log("Queuing second payload", payload2); Logger.send(payload2); /** - * Since autoFlush is disabled, call flush manually. + * Call flush manually. * This will send both payloads in a single * HTTP request. * diff --git a/examples/middleware.js b/examples/retry.js similarity index 68% rename from examples/middleware.js rename to examples/retry.js index 5d3b8c1..fd3a2f1 100644 --- a/examples/middleware.js +++ b/examples/retry.js @@ -15,7 +15,7 @@ */ /** - * This example shows how to use middleware with the SplunkLogger. + * This example shows how to configure retries with SplunkLogger. */ // Change to require("splunk-logging").Logger; @@ -23,20 +23,17 @@ var SplunkLogger = require("../index").Logger; /** * Only the token property is required. - * Defaults are listed explicitly. * - * Alternatively, specify config.url like so: - * - * "https://localhost:8088/services/collector/event/1.0" + * Here we've set maxRetries to 5, + * If there are any connection errors the request to Splunk will + * be retried up to 5 times. + * The default is 0. */ var config = { token: "your-token-here", - host: "localhost", - path: "/services/collector/event/1.0", - protocol: "https", - port: 8088, + url: "https://localhost:8088", level: "info", - autoFlush: true + maxRetries: 5 }; // Create a new logger @@ -47,22 +44,6 @@ Logger.error = function(err, context) { console.log("error", err, "context", context); }; -// Add a middleware function -Logger.use(function(context, next) { - console.log("Message before middleware", context.message); - - // Add a property to the message if it's an object - if (typeof context.message === "object") { - context.message.nestedValue = { - b00l: true, - another: "string" - }; - } - - console.log("Message after middleware", context.message); - next(null, context); -}); - // Define the payload to send to Splunk's Event Collector var payload = { // Message can be anything, doesn't have to be an object diff --git a/splunklogger.js b/splunklogger.js index 3ee674b..965f1b6 100644 --- a/splunklogger.js +++ b/splunklogger.js @@ -27,10 +27,28 @@ var utils = require("./utils"); * @param {object} [context] - The context of an event. * @private */ +/* istanbul ignore next*/ function _err(err, context) { console.log("ERROR:", err, " CONTEXT", context); } +/** + * The default format for Splunk events. + * + * This function can be overwritten, and can return any type (string, object, array, etc.) + * + * @param {anything} [message] - The event message. + * @param {string} [severity] - The event severity. + * @return {any} The event format to send to Splunk, + */ +function _defaultEventFormatter(message, severity) { + var event = { + message: message, + severity: severity + }; + return event; +} + /** * Constructs a SplunkLogger, to send events to Splunk via the HTTP Event Collector. * See defaultConfig for default configuration settings. @@ -41,15 +59,17 @@ function _err(err, context) { * var config = { * token: "your-token-here", * name: "my application", - * host: "splunk.local", - * autoFlush: false + * url: "https://splunk.local:8088" * }; * * var logger = new SplunkLogger(config); * * @property {object} config - Configuration settings for this SplunkLogger instance. - * @property {function[]} middlewares - Middleware functions to run before sending data to Splunk. - * @property {object[]} contextQueue - Queue of context objects to be sent to Splunk. + * @param {object} requestOptions - Options to pass to {@link https://github.com/request/request#requestpost|request.post()}. + * See the {@link http://github.com/request/request|request documentation} for all available options. + * @property {object[]} serializedContextQueue - Queue of serialized context objects to be sent to Splunk. + * @property {function} eventFormatter - Formats events, returning an event as a string, function(message, severity). + * Can be overwritten, the default event formatter will display event and severity as properties in a JSON object. * @property {function} error - A callback function for errors: function(err, context). * Defaults to console.log both values; * @@ -57,6 +77,7 @@ function _err(err, context) { * @param {string} config.token - Splunk HTTP Event Collector token, required. * @param {string} [config.name=splunk-javascript-logging/0.8.0] - Name for this logger. * @param {string} [config.host=localhost] - Hostname or IP address of Splunk server. + * @param {string} [config.maxRetries=0] - How many times to retry when HTTP POST to Splunk fails. * @param {string} [config.path=/services/collector/event/1.0] - URL path to send data to on the Splunk server. * @param {string} [config.protocol=https] - Protocol used to communicate with the Splunk server, http or https. * @param {number} [config.port=8088] - HTTP Event Collector port on the Splunk server. @@ -65,15 +86,37 @@ function _err(err, context) { * the corresponding property is set on config. * @param {string} [config.level=info] - Logging level to use, will show up as the severity field of an event, see * [SplunkLogger.levels]{@link SplunkLogger#levels} for common levels. - * @param {bool} [config.autoFlush=true] - Send events immediately or not. + * @param {number} [config.batchInterval=0] - Automatically flush events after this many milliseconds. + * When set to a non-positive value, events will be sent one by one. This setting is ignored when non-positive. + * @param {number} [config.maxBatchSize=0] - Automatically flush events after the size of queued + * events exceeds this many bytes. This setting is ignored when non-positive. + * @param {number} [config.maxBatchCount=1] - Automatically flush events after this many + * events have been queued. Defaults to flush immediately on sending an event. This setting is ignored when non-positive. * @constructor * @throws Will throw an error if the config parameter is malformed. */ var SplunkLogger = function(config) { + this._timerID = null; + this._timerDuration = 0; this.config = this._initializeConfig(config); - this.middlewares = []; - this.contextQueue = []; + this.requestOptions = this._initializeRequestOptions(); + this.serializedContextQueue = []; + this.eventsBatchSize = 0; + this.eventFormatter = _defaultEventFormatter; this.error = _err; + + this._enableTimer = utils.bind(this, this._enableTimer); + this._disableTimer = utils.bind(this, this._disableTimer); + this._initializeConfig = utils.bind(this, this._initializeConfig); + this._initializeRequestOptions = utils.bind(this, this._initializeRequestOptions); + this._validateMessage = utils.bind(this, this._validateMessage); + this._initializeMetadata = utils.bind(this, this._initializeMetadata); + this._initializeContext = utils.bind(this, this._initializeContext); + this._makeBody = utils.bind(this, this._makeBody); + this._post = utils.bind(this, this._post); + this._sendEvents = utils.bind(this, this._sendEvents); + this.send = utils.bind(this, this.send); + this.flush = utils.bind(this, this.flush); }; /** @@ -97,13 +140,59 @@ var defaultConfig = { protocol: "https", port: 8088, level: SplunkLogger.prototype.levels.INFO, - autoFlush: true + maxRetries: 0, + batchInterval: 0, + maxBatchSize: 0, + maxBatchCount: 1 }; var defaultRequestOptions = { json: true, // Sets the content-type header to application/json - strictSSL: false, - url: defaultConfig.protocol + "://" + defaultConfig.host + ":" + defaultConfig.port + defaultConfig.path + strictSSL: false +}; + +/** + * Disables the interval timer set by this._enableTimer(). + * + * param {Number} interval - The batch interval. + * @private + */ +SplunkLogger.prototype._disableTimer = function() { + if (this._timerID) { + clearInterval(this._timerID); + this._timerDuration = 0; + this._timerID = null; + } +}; + +/** + * Configures an interval timer to flush any events in + * this.serializedContextQueue at the specified interval. + * + * param {Number} interval - The batch interval in milliseconds. + * @private + */ +SplunkLogger.prototype._enableTimer = function(interval) { + // Only enable the timer if possible + interval = utils.validateNonNegativeInt(interval, "Batch interval"); + + if (this._timerID) { + this._disableTimer(); + } + + // If batch interval is changed, update the config property + if (this.config) { + this.config.batchInterval = interval; + } + + this._timerDuration = interval; + + var that = this; + this._timerID = setInterval(function() { + if (that.serializedContextQueue.length > 0) { + that.flush(); + } + }, interval); }; /** @@ -116,12 +205,7 @@ var defaultRequestOptions = { */ SplunkLogger.prototype._initializeConfig = function(config) { // Copy over the instance config - var ret = {}; - for (var key in this.config) { - if (this.config.hasOwnProperty(key)) { - ret[key] = this.config[key]; - } - } + var ret = utils.copyObject(this.config); if (!config) { throw new Error("Config is required."); @@ -162,35 +246,42 @@ SplunkLogger.prototype._initializeConfig = function(config) { } // Take the argument's value, then instance value, then the default value - ret.token = config.token || ret.token; - ret.name = config.name || ret.name || defaultConfig.name; - ret.host = config.host || ret.host || defaultConfig.host; - ret.path = config.path || ret.path || defaultConfig.path; - ret.protocol = config.protocol || ret.protocol || defaultConfig.protocol; - ret.level = config.level || ret.level || defaultConfig.level; - - // Start with the default autoFlush value - ret.autoFlush = defaultConfig.autoFlush; - // Then check this.config.autoFlush - if (this.hasOwnProperty("config") && this.config.hasOwnProperty("autoFlush")) { - ret.autoFlush = ret.autoFlush; - } - // Then check the config.autoFlush, the function argument - if (config.hasOwnProperty("autoFlush")) { - ret.autoFlush = config.autoFlush; + ret.token = utils.orByProp("token", config, ret); + ret.name = utils.orByProp("name", config, ret, defaultConfig); + ret.level = utils.orByProp("level", config, ret, defaultConfig); + + ret.host = utils.orByProp("host", config, ret, defaultConfig); + ret.path = utils.orByProp("path", config, ret, defaultConfig); + ret.protocol = utils.orByProp("protocol", config, ret, defaultConfig); + ret.port = utils.orByProp("port", config, ret, defaultConfig); + ret.port = utils.validateNonNegativeInt(ret.port, "Port"); + if (ret.port < 1000 || ret.port > 65535) { + throw new Error("Port must be an integer between 1000 and 65535, found: " + ret.port); } - if (!config.hasOwnProperty("port")) { - ret.port = ret.port || defaultConfig.port; - } - else { - ret.port = parseInt(config.port, 10); - if (isNaN(ret.port)) { - throw new Error("Port must be an integer, found: " + ret.port); - } + ret.maxRetries = utils.orByProp("maxRetries", config, ret, defaultConfig); + ret.maxRetries = utils.validateNonNegativeInt(ret.maxRetries, "Max retries"); + + // Batching settings + ret.maxBatchCount = utils.orByFalseyProp("maxBatchCount", config, ret, defaultConfig); + ret.maxBatchCount = utils.validateNonNegativeInt(ret.maxBatchCount, "Max batch count"); + ret.maxBatchSize = utils.orByFalseyProp("maxBatchSize", config, ret, defaultConfig); + ret.maxBatchSize = utils.validateNonNegativeInt(ret.maxBatchSize, "Max batch size"); + ret.batchInterval = utils.orByFalseyProp("batchInterval", config, ret, defaultConfig); + ret.batchInterval = utils.validateNonNegativeInt(ret.batchInterval, "Batch interval"); + + // Has the interval timer not started, and needs to be started? + var startTimer = !this._timerID && ret.batchInterval > 0; + // Has the interval timer already started, and the interval changed to a different duration? + var changeTimer = this._timerID && this._timerDuration !== ret.batchInterval && ret.batchInterval > 0; + + // Upsert the timer + if (startTimer || changeTimer) { + this._enableTimer(ret.batchInterval); } - if (ret.port < 1000 || ret.port > 65535) { - throw new Error("Port must be an integer between 1000 and 65535, found: " + ret.port); + // Disable timer - there is currently a timer, but config says we no longer need a timer + else if (this._timerID && (ret.batchInterval <= 0 || this._timerDuration < 0)) { + this._disableTimer(); } } return ret; @@ -205,25 +296,16 @@ SplunkLogger.prototype._initializeConfig = function(config) { * @returns {object} requestOptions * @private */ -SplunkLogger.prototype._initializeRequestOptions = function(config, options) { - var ret = {}; - for (var key in defaultRequestOptions) { - if (defaultRequestOptions.hasOwnProperty(key)) { - ret[key] = defaultRequestOptions[key]; - } - } +SplunkLogger.prototype._initializeRequestOptions = function(options) { + var ret = utils.copyObject(options || defaultRequestOptions); - config = config || this.config || defaultConfig; - options = options || ret; - - ret.url = config.protocol + "://" + config.host + ":" + config.port + config.path; - ret.json = options.hasOwnProperty("json") ? options.json : ret.json; - ret.strictSSL = options.strictSSL || ret.strictSSL; - ret.headers = options.headers || {}; - if (config.token) { - ret.headers.Authorization = "Splunk " + config.token; + if (options) { + ret.json = options.hasOwnProperty("json") ? options.json : defaultRequestOptions.json; + ret.strictSSL = options.strictSSL || defaultRequestOptions.strictSSL; } + ret.headers = ret.headers || {}; + return ret; }; @@ -233,7 +315,7 @@ SplunkLogger.prototype._initializeRequestOptions = function(config, options) { * @private * @throws Will throw an error if the message parameter is malformed. */ -SplunkLogger.prototype._initializeMessage = function(message) { +SplunkLogger.prototype._validateMessage = function(message) { if (typeof message === "undefined" || message === null) { throw new Error("Message argument is required."); } @@ -242,7 +324,7 @@ SplunkLogger.prototype._initializeMessage = function(message) { /** * Initialized metadata, if context.metadata is falsey or empty, - * return an empty object; + * return an empty object. * * @param {object} context * @returns {object} metadata @@ -250,7 +332,7 @@ SplunkLogger.prototype._initializeMessage = function(message) { */ SplunkLogger.prototype._initializeMetadata = function(context) { var metadata = {}; - if (context.hasOwnProperty("metadata")) { + if (context && context.hasOwnProperty("metadata")) { if (context.metadata.hasOwnProperty("time")) { metadata.time = context.metadata.time; } @@ -271,7 +353,7 @@ SplunkLogger.prototype._initializeMetadata = function(context) { }; /** - * Initializes a context. + * Initializes a context object. * * @param context * @returns {object} context @@ -289,15 +371,9 @@ SplunkLogger.prototype._initializeContext = function(context) { throw new Error("Context argument must have the message property set."); } - // _initializeConfig will throw an error config or this.config is - // undefined, or doesn't have at least the token property set - context.config = this._initializeConfig(context.config || this.config); - - context.requestOptions = this._initializeRequestOptions(context.config, context.requestOptions); + context.message = this._validateMessage(context.message); - context.message = this._initializeMessage(context.message); - - context.severity = context.severity || SplunkLogger.prototype.levels.INFO; + context.severity = context.severity || defaultConfig.level; context.metadata = context.metadata || this._initializeMetadata(context); @@ -320,47 +396,24 @@ SplunkLogger.prototype._makeBody = function(context) { var body = this._initializeMetadata(context); var time = utils.formatTime(body.time || Date.now()); body.time = time.toString(); - body.event = { - message: context.message, - severity: context.severity || SplunkLogger.prototype.levels.INFO - }; - + + body.event = this.eventFormatter(context.message, context.severity || defaultConfig.level); return body; }; /** - * Adds an express-like middleware function to run before sending the - * data to Splunk. - * Multiple middleware functions can be used, they will be executed - * in the order they are added. - * - * This function is a wrapper around this.middlewares.push(). - * - * @example - * var SplunkLogger = require("splunk-logging").Logger; - * - * var Logger = new SplunkLogger({token: "your-token-here"}); - * Logger.use(function(context, next) { - * context.message.additionalProperty = "Add this before sending the data"; - * next(null, context); - * }); + * Makes an HTTP POST to the configured server. * - * @param {function} middleware - A middleware function: function(context, next). - * It must call next(error, context) to continue. - * @public - * @throws Will throw an error if middleware is not a function. + * @param requestOptions + * @param {function} callback = A callback function: function(err, response, body). + * @private */ -SplunkLogger.prototype.use = function(middleware) { - if (!middleware || typeof middleware !== "function") { - throw new Error("Middleware must be a function."); - } - else { - this.middlewares.push(middleware); - } +SplunkLogger.prototype._post = function(requestOptions, callback) { + request.post(requestOptions, callback); }; /** - * Makes an HTTP POST to the configured server. + * Sends events to Splunk, optionally with retries on non-Splunk errors. * * @param context * @param {function} callback - A callback function: function(err, response, body) @@ -369,38 +422,75 @@ SplunkLogger.prototype.use = function(middleware) { SplunkLogger.prototype._sendEvents = function(context, callback) { callback = callback || /* istanbul ignore next*/ function(){}; - // Validate the context again, right before using it + // Initialize the config once more to avoid undefined vals below + this.config = this._initializeConfig(this.config); + + // Makes a copy of the request options so we can set the body + var requestOptions = this._initializeRequestOptions(this.requestOptions); + requestOptions.body = this._validateMessage(context.message); + requestOptions.headers["Authorization"] = "Splunk " + this.config.token; + // Manually set the content-type header, the default is application/json + // since json is set to true. + requestOptions.headers["Content-Type"] = "application/x-www-form-urlencoded"; + requestOptions.url = this.config.protocol + "://" + this.config.host + ":" + this.config.port + this.config.path; + + + // Initialize the context again, right before using it context = this._initializeContext(context); - context.requestOptions.headers["Authorization"] = "Splunk " + context.config.token; - if (context.config.autoFlush) { - context.requestOptions.body = this._makeBody(context); - } - else { - // Don't run _makeBody since we've already done that - context.requestOptions.body = context.message; - // Manually set the content-type header for batched requests, default is application/json - // since json is set to true. - context.requestOptions.headers["content-type"] = "application/x-www-form-urlencoded"; - } var that = this; - request.post(context.requestOptions, function(err, resp, body) { - // Call error() if error, or body isn't success - var error = err; - // Assume this is a non-success response from Splunk, build the error accordingly - if (!err && body && body.code.toString() !== "0") { - error = new Error(body.text); - error.code = body.code; - } - if (error) { - that.error(error, context); + + var splunkError = null; // Errors returned by Splunk + var requestError = null; // Any non-Splunk errors + + // References so we don't have to deal with callback parameters + var _response = null; + var _body = null; + + var numRetries = 0; + + utils.whilst( + function() { + // Continue if we can (re)try + return numRetries++ <= that.config.maxRetries; + }, + function(done) { + that._post(requestOptions, function(err, resp, body) { + // Store the latest error, response & body + splunkError = null; + requestError = err; + _response = resp; + _body = body; + + // Try to parse an error response from Splunk + if (!requestError && body && body.code.toString() !== "0") { + splunkError = new Error(body.text); + splunkError.code = body.code; + } + + // Retry if no Splunk error, a non-200 request response, and numRetries hasn't exceeded the limit + if (!splunkError && requestError && numRetries <= that.config.maxRetries) { + utils.expBackoff({attempt: numRetries}, done); + } + else { + // Stop iterating + done(true); + } + }); + }, + function() { + // Call error() for a request error or Splunk error + if (requestError || splunkError) { + that.error(requestError || splunkError, context); + } + + callback(requestError, _response, _body); } - callback(err, resp, body); - }); + ); }; - + /** - * Sends or queues data to be sent based on context.config.autoFlush. + * Sends or queues data to be sent based on batching settings. * Default behavior is to send immediately. * * @example @@ -418,7 +508,7 @@ SplunkLogger.prototype._sendEvents = function(context, callback) { * chickenCount: 500 * }, * severity: "info", - * { + * metadata: { * source: "chicken coop", * sourcetype: "httpevent", * index: "main", @@ -426,6 +516,8 @@ SplunkLogger.prototype._sendEvents = function(context, callback) { * } * }; * + * // The callback is only used if maxBatchCount=1, or + * // batching thresholds have been exceeded. * logger.send(payload, function(err, resp, body) { * if (err) { * console.log("error:", err); @@ -436,10 +528,6 @@ SplunkLogger.prototype._sendEvents = function(context, callback) { * * @param {object} context - An object with at least the data property. * @param {(object|string|Array|number|bool)} context.message - Data to send to Splunk. - * @param {object} [context.requestOptions] - Defaults are {json:true, strictSSL:false}. Additional - * options to pass to {@link https://github.com/request/request#requestpost|request.post()}. - * See the {@link http://github.com/request/request|request documentation} for all available options. - * @param {object} [context.config] - See {@link SplunkLogger} for default values. * @param {string} [context.severity=info] - Severity level of this event. * @param {object} [context.metadata] - Metadata for this event. * @param {string} [context.metadata.host] - If not specified, Splunk will decide the value. @@ -451,82 +539,44 @@ SplunkLogger.prototype._sendEvents = function(context, callback) { * @throws Will throw an error if the context parameter is malformed. * @public */ -SplunkLogger.prototype.send = function (context, callback) { - callback = callback || function(){}; +SplunkLogger.prototype.send = function(context, callback) { context = this._initializeContext(context); - this.contextQueue.push(context); + // Store the context, and its estimated length + var currentEvent = JSON.stringify(this._makeBody(context)); + this.serializedContextQueue.push(currentEvent); + this.eventsBatchSize += Buffer.byteLength(currentEvent, "utf8"); - if (context.config.autoFlush) { - this.flush(callback); - } - else { - callback(); + var batchOverSize = this.eventsBatchSize > this.config.maxBatchSize && this.config.maxBatchSize > 0; + var batchOverCount = this.serializedContextQueue.length >= this.config.maxBatchCount && this.config.maxBatchCount > 0; + + // Only flush if the queue's byte size is too large, or has too many events + if (batchOverSize || batchOverCount) { + this.flush(callback || function(){}); } }; /** - * Manually send events in this.contextQueue to Splunk, after - * chaining any functions in this.middlewares. - * Auto flush settings will be used from this.config.autoFlush, - * ignoring auto flush settings every on every context in this.contextQueue. + * Manually send all events in this.serializedContextQueue to Splunk. * * @param {function} [callback] - A callback function: function(err, response, body). * @public */ -SplunkLogger.prototype.flush = function (callback) { +SplunkLogger.prototype.flush = function(callback) { callback = callback || function(){}; - var context = {}; + // Empty the queue, reset the eventsBatchSize + var queue = this.serializedContextQueue; + this.serializedContextQueue = []; + this.eventsBatchSize = 0; - // Use the batching setting from this.config - if (this.config.autoFlush) { - // TODO: handle case of multiple events with autoFlush off, flushing fast - // Just take the oldest event in the queue - context = this.contextQueue.pop(); - } - else { - // Empty the event queue - var queue = this.contextQueue; - this.contextQueue = []; - var data = ""; - for (var i = 0; i < queue.length; i++) { - data += JSON.stringify(this._makeBody(queue[i])); - } - context.message = data; - } - - // Initialize the context, then manually set the data - context = this._initializeContext(context); + // Send all queued events + var data = queue.join(""); + var context = { + message: data + }; - // Copy over the middlewares - var callbacks = []; - for (var j = 0; j < this.middlewares.length; j++) { - callbacks[j] = this.middlewares[j]; - } - - // Send the data to the first middleware - callbacks.unshift(function(cb) { - cb(null, context); - }); - - // After running all, if any, middlewares send the events - var that = this; - utils.chain(callbacks, function(err, passedContext) { - // Errors from any of the middleware callbacks will fall through to here - // If the context is modified at any point the error callback will get it also - // event if next("error"); is called w/o the context parameter! - // This works because context inside & outside the scope of this function - // point to the same memory block. - // The passedContext parameter could be named context to - // do this automatically, but the || notation adds a bit of clarity. - if (err) { - that.error(err, passedContext || context); - } - else { - that._sendEvents(context, callback); - } - }); + this._sendEvents(context, callback); }; module.exports = SplunkLogger; \ No newline at end of file diff --git a/test/test_config.js b/test/test_config.js index 12ac35b..f6f7ecc 100644 --- a/test/test_config.js +++ b/test/test_config.js @@ -100,7 +100,7 @@ describe("SplunkLogger", function() { } catch (err) { assert.ok(err); - assert.strictEqual(err.message, "Port must be an integer, found: NaN"); + assert.strictEqual(err.message, "Port must be a number, found: NaN"); } }); it("should correctly parse port with leading zero", function() { @@ -141,12 +141,25 @@ describe("SplunkLogger", function() { assert.strictEqual("info", logger.config.level); assert.strictEqual(logger.levels.INFO, logger.config.level); assert.strictEqual(8088, logger.config.port); + assert.strictEqual(0, logger.config.maxRetries); + assert.strictEqual(0, logger.config.batchInterval); + assert.strictEqual(0, logger.config.maxBatchSize); + + var expectedRO = { + json: true, + strictSSL: false, + headers: {} + }; + assert.ok(logger.hasOwnProperty("requestOptions")); + assert.strictEqual(Object.keys(logger.requestOptions).length, 3); + assert.strictEqual(expectedRO.json, logger.requestOptions.json); + assert.strictEqual(expectedRO.strictSSL, logger.requestOptions.strictSSL); + assert.strictEqual(Object.keys(expectedRO.headers).length, Object.keys(logger.requestOptions.headers).length); }); - it("should set remaining defaults when setting config with token, autoFlush off, & level", function() { + it("should set remaining defaults when setting config with token, batching off, & level", function() { var config = { token: "a-token-goes-here-usually", - level: "important", - autoFlush: false + level: "important" }; var logger = new SplunkLogger(config); @@ -157,8 +170,195 @@ describe("SplunkLogger", function() { assert.strictEqual("/services/collector/event/1.0", logger.config.path); assert.strictEqual("https", logger.config.protocol); assert.strictEqual("important", logger.config.level); - assert.strictEqual(false, logger.config.autoFlush); assert.strictEqual(8088, logger.config.port); + assert.strictEqual(0, logger.config.maxRetries); + }); + it("should error when _enableTimer(NaN)", function() { + var config = { + token: "a-token-goes-here-usually" + }; + + try { + var logger = new SplunkLogger(config); + logger._enableTimer("not a number"); + assert.fail(!logger, "Expected an error."); + } + catch (err) { + assert.ok(err); + assert.strictEqual("Batch interval must be a number, found: NaN", err.message); + } + }); + it("should error when batchInterval=NaN", function() { + var config = { + token: "a-token-goes-here-usually", + batchInterval: "not a number", + }; + + try { + var logger = new SplunkLogger(config); + assert.fail(!logger, "Expected an error."); + } + catch (err) { + assert.ok(err); + assert.strictEqual("Batch interval must be a number, found: NaN", err.message); + } + }); + it("should error when batchInterval is negative", function() { + var config = { + token: "a-token-goes-here-usually", + batchInterval: -1, + }; + + try { + var logger = new SplunkLogger(config); + assert.fail(!logger, "Expected an error."); + } + catch (err) { + assert.ok(err); + assert.strictEqual("Batch interval must be a positive number, found: -1", err.message); + } + }); + it("should change the timer via _enableTimer()", function() { + var config = { + token: "a-token-goes-here-usually" + }; + + var logger = new SplunkLogger(config); + logger._enableTimer(1); + assert.strictEqual(logger._timerDuration, 1); + logger._enableTimer(2); + assert.strictEqual(logger._timerDuration, 2); + logger._disableTimer(); + }); + // TODO: fix this test + it("should disable the timer via _initializeConfig()", function() { + var config = { + token: "a-token-goes-here-usually" + }; + + var logger = new SplunkLogger(config); + logger._enableTimer(1); + assert.strictEqual(logger._timerDuration, 1); + logger._enableTimer(2); + assert.strictEqual(logger._timerDuration, 2); + + logger.config.batchInterval = 0; + logger.config.maxBatchCount = 0; + + logger._initializeConfig(logger.config); + assert.ok(!logger._timerDuration); + assert.ok(!logger._timerID); + }); + it("should be noop when _disableTimer() is called when no timer is configured", function() { + var config = { + token: "a-token-goes-here-usually" + }; + + var logger = new SplunkLogger(config); + var old = logger._timerDuration; + assert.ok(!logger._timerDuration); + logger._disableTimer(); + assert.ok(!logger._timerDuration); + assert.strictEqual(logger._timerDuration, old); + }); + it("should set a batch interval timer with batching on, & batchInterval set", function() { + var config = { + token: "a-token-goes-here-usually", + batchInterval: 100 + }; + var logger = new SplunkLogger(config); + + assert.ok(logger); + assert.ok(logger._timerID); + + assert.strictEqual(config.token, logger.config.token); + assert.strictEqual("splunk-javascript-logging/0.8.0", logger.config.name); + assert.strictEqual("localhost", logger.config.host); + assert.strictEqual("/services/collector/event/1.0", logger.config.path); + assert.strictEqual("https", logger.config.protocol); + assert.strictEqual("info", logger.config.level); + assert.strictEqual(100, logger.config.batchInterval); + assert.strictEqual(8088, logger.config.port); + assert.strictEqual(0, logger.config.maxRetries); + }); + it("should not set a batch interval timer with batching on, & default batchInterval", function() { + var config = { + token: "a-token-goes-here-usually" + }; + var logger = new SplunkLogger(config); + + assert.ok(logger); + assert.ok(!logger._timerID); + + assert.strictEqual(config.token, logger.config.token); + assert.strictEqual("splunk-javascript-logging/0.8.0", logger.config.name); + assert.strictEqual("localhost", logger.config.host); + assert.strictEqual("/services/collector/event/1.0", logger.config.path); + assert.strictEqual("https", logger.config.protocol); + assert.strictEqual("info", logger.config.level); + assert.strictEqual(0, logger.config.batchInterval); + assert.strictEqual(8088, logger.config.port); + assert.strictEqual(0, logger.config.maxRetries); + }); + it("should error when maxBatchCount=NaN", function() { + var config = { + token: "a-token-goes-here-usually", + maxBatchCount: "not a number", + }; + + try { + var logger = new SplunkLogger(config); + assert.fail(!logger, "Expected an error."); + } + catch (err) { + assert.ok(err); + assert.strictEqual("Max batch count must be a number, found: NaN", err.message); + } + }); + it("should error when maxBatchCount is negative", function() { + var config = { + token: "a-token-goes-here-usually", + maxBatchCount: -1, + }; + + try { + var logger = new SplunkLogger(config); + assert.fail(!logger, "Expected an error."); + } + catch (err) { + assert.ok(err); + assert.strictEqual("Max batch count must be a positive number, found: -1", err.message); + } + }); + it("should error when maxBatchSize=NaN", function() { + var config = { + token: "a-token-goes-here-usually", + maxBatchSize: "not a number", + }; + + try { + var logger = new SplunkLogger(config); + assert.fail(!logger, "Expected an error."); + } + catch (err) { + assert.ok(err); + assert.strictEqual("Max batch size must be a number, found: NaN", err.message); + } + }); + it("should error when maxBatchSize is negative", function() { + var config = { + token: "a-token-goes-here-usually", + maxBatchSize: -1, + }; + + try { + var logger = new SplunkLogger(config); + assert.fail(!logger, "Expected an error."); + } + catch (err) { + assert.ok(err); + assert.strictEqual("Max batch size must be a positive number, found: -1", err.message); + } }); it("should set non-default boolean config values", function() { var config = { @@ -169,6 +369,8 @@ describe("SplunkLogger", function() { var logger = new SplunkLogger(config); assert.ok(logger); + assert.ok(!logger._timerID); + assert.strictEqual(config.token, logger.config.token); assert.strictEqual("splunk-javascript-logging/0.8.0", logger.config.name); assert.strictEqual("localhost", logger.config.host); @@ -176,6 +378,7 @@ describe("SplunkLogger", function() { assert.strictEqual("http", logger.config.protocol); assert.strictEqual("info", logger.config.level); assert.strictEqual(8088, logger.config.port); + assert.strictEqual(0, logger.config.maxRetries); }); it("should set non-default path", function() { var config = { @@ -192,6 +395,7 @@ describe("SplunkLogger", function() { assert.strictEqual("https", logger.config.protocol); assert.strictEqual("info", logger.config.level); assert.strictEqual(8088, logger.config.port); + assert.strictEqual(0, logger.config.maxRetries); }); it("should set non-default port", function() { var config = { @@ -208,6 +412,25 @@ describe("SplunkLogger", function() { assert.strictEqual("https", logger.config.protocol); assert.strictEqual("info", logger.config.level); assert.strictEqual(config.port, logger.config.port); + assert.strictEqual(0, logger.config.maxRetries); + }); + it("should set non-default maxBatchSize", function() { + var config = { + token: "a-token-goes-here-usually", + maxBatchSize: 1234 + }; + var logger = new SplunkLogger(config); + + assert.ok(logger); + assert.strictEqual(config.token, logger.config.token); + assert.strictEqual("splunk-javascript-logging/0.8.0", logger.config.name); + assert.strictEqual("localhost", logger.config.host); + assert.strictEqual("/services/collector/event/1.0", logger.config.path); + assert.strictEqual("https", logger.config.protocol); + assert.strictEqual("info", logger.config.level); + assert.strictEqual(8088, logger.config.port); + assert.strictEqual(0, logger.config.maxRetries); + assert.strictEqual(1234, logger.config.maxBatchSize); }); it("should set protocol, host, port & path from url property", function() { var config = { @@ -224,6 +447,7 @@ describe("SplunkLogger", function() { assert.strictEqual("http", logger.config.protocol); assert.strictEqual("info", logger.config.level); assert.strictEqual(9088, logger.config.port); + assert.strictEqual(0, logger.config.maxRetries); }); it("should set protocol from url property", function() { var config = { @@ -240,6 +464,7 @@ describe("SplunkLogger", function() { assert.strictEqual("http", logger.config.protocol); assert.strictEqual("info", logger.config.level); assert.strictEqual(8088, logger.config.port); + assert.strictEqual(0, logger.config.maxRetries); }); it("should set everything but path from url property", function() { var config = { @@ -256,6 +481,7 @@ describe("SplunkLogger", function() { assert.strictEqual("http", logger.config.protocol); assert.strictEqual("info", logger.config.level); assert.strictEqual(9088, logger.config.port); + assert.strictEqual(0, logger.config.maxRetries); }); it("should set everything but path from url property with trailing slash", function() { var config = { @@ -272,6 +498,7 @@ describe("SplunkLogger", function() { assert.strictEqual("http", logger.config.protocol); assert.strictEqual("info", logger.config.level); assert.strictEqual(9088, logger.config.port); + assert.strictEqual(0, logger.config.maxRetries); }); it("should set host from url property with host only", function() { var config = { @@ -287,8 +514,25 @@ describe("SplunkLogger", function() { assert.strictEqual("/services/collector/event/1.0", logger.config.path); assert.strictEqual("https", logger.config.protocol); assert.strictEqual("info", logger.config.level); - assert.strictEqual(true, logger.config.autoFlush); assert.strictEqual(8088, logger.config.port); + assert.strictEqual(0, logger.config.maxRetries); + }); + it("should set maxRetries", function() { + var config = { + token: "a-token-goes-here-usually", + maxRetries: 10 + }; + var logger = new SplunkLogger(config); + + assert.ok(logger); + assert.strictEqual(config.token, logger.config.token); + assert.strictEqual("splunk-javascript-logging/0.8.0", logger.config.name); + assert.strictEqual("localhost", logger.config.host); + assert.strictEqual("/services/collector/event/1.0", logger.config.path); + assert.strictEqual("https", logger.config.protocol); + assert.strictEqual("info", logger.config.level); + assert.strictEqual(8088, logger.config.port); + assert.strictEqual(10, logger.config.maxRetries); }); }); describe("_initializeConfig", function() { @@ -338,6 +582,36 @@ describe("SplunkLogger", function() { assert.strictEqual(err.message, "Config token must be a string."); } }); + it("should error with NaN maxRetries", function() { + var config = { + token: "a-token-goes-here-usually", + maxRetries: "this isn't a number" + }; + + try { + SplunkLogger.prototype._initializeConfig(config); + assert.fail(false, "Expected an error."); + } + catch (err) { + assert.ok(err); + assert.strictEqual(err.message, "Max retries must be a number, found: NaN"); + } + }); + it("should error with negative maxRetries", function() { + var config = { + token: "a-token-goes-here-usually", + maxRetries: -1 + }; + + try { + SplunkLogger.prototype._initializeConfig(config); + assert.fail(false, "Expected an error."); + } + catch (err) { + assert.ok(err); + assert.strictEqual(err.message, "Max retries must be a positive number, found: -1"); + } + }); it("should error with NaN port", function() { var config = { token: "a-token-goes-here-usually", @@ -350,7 +624,7 @@ describe("SplunkLogger", function() { } catch (err) { assert.ok(err); - assert.strictEqual(err.message, "Port must be an integer, found: NaN"); + assert.strictEqual(err.message, "Port must be a number, found: NaN"); } }); it("should correctly parse port with leading zero", function() { @@ -390,6 +664,7 @@ describe("SplunkLogger", function() { assert.strictEqual("https", loggerConfig.protocol); assert.strictEqual("info", loggerConfig.level); assert.strictEqual(8088, loggerConfig.port); + assert.strictEqual(0, loggerConfig.maxRetries); }); it("should set non-default boolean config values", function() { var config = { @@ -406,6 +681,7 @@ describe("SplunkLogger", function() { assert.strictEqual("http", loggerConfig.protocol); assert.strictEqual("info", loggerConfig.level); assert.strictEqual(8088, loggerConfig.port); + assert.strictEqual(0, loggerConfig.maxRetries); }); it("should set non-default path", function() { var config = { @@ -422,6 +698,7 @@ describe("SplunkLogger", function() { assert.strictEqual("https", loggerConfig.protocol); assert.strictEqual("info", loggerConfig.level); assert.strictEqual(8088, loggerConfig.port); + assert.strictEqual(0, loggerConfig.maxRetries); }); it("should set non-default port", function() { var config = { @@ -438,6 +715,7 @@ describe("SplunkLogger", function() { assert.strictEqual("https", loggerConfig.protocol); assert.strictEqual("info", loggerConfig.level); assert.strictEqual(config.port, loggerConfig.port); + assert.strictEqual(0, loggerConfig.maxRetries); }); it("should set protocol, host, port & path from url property", function() { var config = { @@ -454,6 +732,7 @@ describe("SplunkLogger", function() { assert.strictEqual("http", loggerConfig.protocol); assert.strictEqual("info", loggerConfig.level); assert.strictEqual(9088, loggerConfig.port); + assert.strictEqual(0, loggerConfig.maxRetries); }); it("should set protocol from url property", function() { var config = { @@ -470,6 +749,7 @@ describe("SplunkLogger", function() { assert.strictEqual("http", loggerConfig.protocol); assert.strictEqual("info", loggerConfig.level); assert.strictEqual(8088, loggerConfig.port); + assert.strictEqual(0, loggerConfig.maxRetries); }); it("should set host from url property with host only", function() { var config = { @@ -486,6 +766,7 @@ describe("SplunkLogger", function() { assert.strictEqual("https", loggerConfig.protocol); assert.strictEqual("info", loggerConfig.level); assert.strictEqual(8088, loggerConfig.port); + assert.strictEqual(0, loggerConfig.maxRetries); }); it("should ignore prototype values", function() { Object.prototype.something = "ignore"; @@ -504,128 +785,59 @@ describe("SplunkLogger", function() { assert.strictEqual("https", loggerConfig.protocol); assert.strictEqual("info", loggerConfig.level); assert.strictEqual(8088, loggerConfig.port); + assert.strictEqual(0, loggerConfig.maxRetries); }); }); describe("_initializeRequestOptions", function() { it("should get defaults with no args", function() { var options = SplunkLogger.prototype._initializeRequestOptions(); assert.ok(options); + assert.ok(Object.keys(options).length, 3); assert.strictEqual(options.json, true); assert.strictEqual(options.strictSSL, false); - assert.strictEqual(options.url, "https://localhost:8088/services/collector/event/1.0"); - assert.ok(options.hasOwnProperty("headers")); - assert.strictEqual(Object.keys(options.headers).length, 0); - assert.ok(!options.headers.hasOwnProperty("Authorization")); - }); - it("should create default options with token in config", function() { - var config = { - token: "some-value" - }; - // Get the defaults because we're passing in a config - config = SplunkLogger.prototype._initializeConfig(config); - - var options = SplunkLogger.prototype._initializeRequestOptions(config); - assert.ok(options); - assert.strictEqual(options.url, "https://localhost:8088/services/collector/event/1.0"); assert.ok(options.headers); - assert.ok(options.headers.hasOwnProperty("Authorization")); - assert.ok(options.headers.Authorization, "Splunk " + config.token); - assert.strictEqual(options.json, true); - assert.strictEqual(options.strictSSL, false); + assert.strictEqual(Object.keys(options.headers).length, 0); }); - it("should create options with full config", function() { - var config = { - token: "some-value", - protocol: "http", - host: "splunk.local", - port: 1234, - path: "/services/collector/custom/1.0" + it("should get defaults with none of the default props configured", function() { + var optionsOriginal = { + something: "here", + value: 1234 }; - config = SplunkLogger.prototype._initializeConfig(config); - - var options = SplunkLogger.prototype._initializeRequestOptions(config); + var options = SplunkLogger.prototype._initializeRequestOptions(optionsOriginal); assert.ok(options); - assert.strictEqual(options.url, "http://splunk.local:1234/services/collector/custom/1.0"); - assert.ok(options.headers); - assert.ok(options.headers.hasOwnProperty("Authorization")); - assert.ok(options.headers.Authorization, "Splunk " + config.token); + assert.ok(Object.keys(options).length, 5); assert.strictEqual(options.json, true); assert.strictEqual(options.strictSSL, false); - }); - it("should create options with full config, empty options", function() { - var config = { - token: "some-value", - protocol: "http", - host: "splunk.local", - port: 1234, - path: "/services/collector/custom/1.0" - }; - config = SplunkLogger.prototype._initializeConfig(config); - - var options = SplunkLogger.prototype._initializeRequestOptions(config, {}); - assert.ok(options); - assert.strictEqual(options.url, "http://splunk.local:1234/services/collector/custom/1.0"); + assert.strictEqual(options.something, optionsOriginal.something); + assert.strictEqual(options.value, optionsOriginal.value); assert.ok(options.headers); - assert.ok(options.headers.hasOwnProperty("Authorization")); - assert.ok(options.headers.Authorization, "Splunk " + config.token); - assert.strictEqual(options.json, true); - assert.strictEqual(options.strictSSL, false); + assert.strictEqual(Object.keys(options.headers).length, 0); }); - - it("should create options with full config, & full options", function() { - var config = { - token: "some-value", - protocol: "http", - host: "splunk.local", - port: 1234, - path: "/services/collector/custom/1.0" - }; - config = SplunkLogger.prototype._initializeConfig(config); - - var initialOptions = { + it("should get defaults with non-default values", function() { + var optionsOriginal = { json: false, strictSSL: true, - url: "should be overwritten", headers: { - Custom: "header-value", - Authorization: "Should be overwritten" - } + Authorization: "nothing" + }, + dummy: "value" }; - var options = SplunkLogger.prototype._initializeRequestOptions(config, initialOptions); + var options = SplunkLogger.prototype._initializeRequestOptions(optionsOriginal); assert.ok(options); - assert.strictEqual(options.url, "http://splunk.local:1234/services/collector/custom/1.0"); - assert.ok(options.headers); - assert.ok(options.headers.hasOwnProperty("Custom")); - assert.strictEqual(options.headers.Custom, initialOptions.headers.Custom); - assert.ok(options.headers.hasOwnProperty("Authorization")); - assert.ok(options.headers.Authorization, "Splunk " + config.token); + assert.ok(Object.keys(options).length, 4); assert.strictEqual(options.json, false); assert.strictEqual(options.strictSSL, true); - }); - it("should create default options with token in config", function() { - Object.prototype.someproperty = "ignore"; - var config = { - token: "some-value" - }; - // Get the defaults because we're passing in a config - config = SplunkLogger.prototype._initializeConfig(config); - - var options = SplunkLogger.prototype._initializeRequestOptions(config); - assert.ok(options); - assert.ok(!options.hasOwnProperty("someproperty")); - assert.strictEqual(options.url, "https://localhost:8088/services/collector/event/1.0"); + assert.strictEqual(options.dummy, "value"); assert.ok(options.headers); - assert.ok(options.headers.hasOwnProperty("Authorization")); - assert.ok(options.headers.Authorization, "Splunk " + config.token); - assert.strictEqual(options.json, true); - assert.strictEqual(options.strictSSL, false); + assert.strictEqual(Object.keys(options.headers).length, 1); + assert.strictEqual(options.headers["Authorization"], "nothing"); }); }); - describe("_initializeMessage", function() { + describe("_validateMessage", function() { it("should error with no args", function() { try { - SplunkLogger.prototype._initializeMessage(); + SplunkLogger.prototype._validateMessage(); assert.ok(false, "Expected an error."); } catch (err) { @@ -635,7 +847,7 @@ describe("SplunkLogger", function() { }); it("should leave string intact", function() { var beforeMessage = "something"; - var afterMessage = SplunkLogger.prototype._initializeMessage(beforeMessage); + var afterMessage = SplunkLogger.prototype._validateMessage(beforeMessage); assert.ok(afterMessage); assert.strictEqual(afterMessage, beforeMessage); }); @@ -671,52 +883,17 @@ describe("SplunkLogger", function() { assert.strictEqual(err.message, "Context argument must have the message property set."); } }); - it("should error with data only", function() { - try { - var context = { - message: "something" - }; - SplunkLogger.prototype._initializeContext(context); - assert.ok(false, "Expected an error."); - } - catch(err) { - assert.ok(err); - assert.strictEqual(err.message, "Config is required."); - } - }); - it("should succeed with default context, specifying data & config token", function() { + it("should succeed with default context, specifying a string message", function() { var context = { - message: "some data", - config: { - token: "a-token-goes-here-usually" - } + message: "some data" }; var initialized = SplunkLogger.prototype._initializeContext(context); var data = initialized.message; - var config = initialized.config; - var requestOptions = initialized.requestOptions; assert.ok(initialized); assert.ok(data); assert.strictEqual(data, context.message); - - assert.ok(config); - assert.strictEqual(config.token, context.config.token); - assert.strictEqual(config.name, "splunk-javascript-logging/0.8.0"); - assert.strictEqual(config.host, "localhost"); - assert.strictEqual(config.path, "/services/collector/event/1.0"); - assert.strictEqual(config.protocol, "https"); - assert.strictEqual(config.level, "info"); - assert.strictEqual(config.port, 8088); - - assert.ok(requestOptions); - assert.strictEqual(requestOptions.json, true); - assert.strictEqual(requestOptions.strictSSL, false); - assert.strictEqual(requestOptions.url, "https://localhost:8088/services/collector/event/1.0"); - assert.ok(requestOptions.hasOwnProperty("headers")); - assert.strictEqual(Object.keys(requestOptions.headers).length, 1); - assert.strictEqual(requestOptions.headers.Authorization, "Splunk " + context.config.token); }); }); describe("constructor + _initializeConfig", function() { @@ -744,7 +921,6 @@ describe("SplunkLogger", function() { assert.strictEqual(Logger.config.protocol, expected.protocol); assert.strictEqual(Logger.config.level, expected.level); assert.strictEqual(Logger.config.port, expected.port); - assert.strictEqual(Logger.middlewares.length, 0); Logger._initializeConfig({}); assert.strictEqual(Logger.config.token, expected.token); @@ -754,7 +930,6 @@ describe("SplunkLogger", function() { assert.strictEqual(Logger.config.protocol, expected.protocol); assert.strictEqual(Logger.config.level, expected.level); assert.strictEqual(Logger.config.port, expected.port); - assert.strictEqual(Logger.middlewares.length, 0); }); it("token in constructor, then init with full config", function() { var config = { @@ -780,7 +955,6 @@ describe("SplunkLogger", function() { assert.strictEqual(Logger.config.protocol, expected.protocol); assert.strictEqual(Logger.config.level, expected.level); assert.strictEqual(Logger.config.port, expected.port); - assert.strictEqual(Logger.middlewares.length, 0); expected.token = "a-different-token"; Logger.config = Logger._initializeConfig(expected); @@ -791,7 +965,6 @@ describe("SplunkLogger", function() { assert.strictEqual(Logger.config.protocol, expected.protocol); assert.strictEqual(Logger.config.level, expected.level); assert.strictEqual(Logger.config.port, expected.port); - assert.strictEqual(Logger.middlewares.length, 0); }); }); }); diff --git a/test/test_send.js b/test/test_send.js index dc66f46..6308d8b 100644 --- a/test/test_send.js +++ b/test/test_send.js @@ -41,34 +41,11 @@ var noDataBody = { code: 5 }; -// TODO: add a test that gets this response -// var incorrectIndexBody = { -// text: "Incorrect index", -// code: 7, -// "invalid-event-number": 1 -// }; - -// Backup console.log so we can restore it later -var ___log = console.log; -/** - * Silences console.log - * Undo this effect by calling unmute(). - */ -function mute() { - console.log = function(){}; -} - -/** - * Un-silences console.log - */ -function unmute() { - console.log = ___log; -} - -describe("SplunkLogger _makedata", function() { +describe("SplunkLogger _makeBody", function() { it("should error with no args", function() { try { - SplunkLogger.prototype._makeBody(); + var logger = new SplunkLogger({token: "token-goes-here"}); + logger._makeBody(); assert.ok(false, "Expected an error."); } catch(err) { @@ -80,27 +57,32 @@ describe("SplunkLogger _makedata", function() { var context = { message: "something" }; - var body = SplunkLogger.prototype._makeBody(context); + var logger = new SplunkLogger({token: "token-goes-here"}); + var body = logger._makeBody(context); assert.ok(body); assert.ok(body.hasOwnProperty("event")); assert.strictEqual(Object.keys(body).length, 2); - assert.ok(body.event.hasOwnProperty("message")); - assert.strictEqual(body.event.message, context.message); - assert.strictEqual(body.event.severity, "info"); + var event = body.event; + assert.ok(event.hasOwnProperty("message")); + assert.strictEqual(event.message, context.message); + assert.strictEqual(event.severity, "info"); }); it("should objectify data as array, without severity param", function() { var context = { message: ["something"] }; - var body = SplunkLogger.prototype._makeBody(context); + var logger = new SplunkLogger({token: "token-goes-here"}); + var body = logger._makeBody(context); assert.ok(body); assert.ok(body.hasOwnProperty("event")); assert.strictEqual(Object.keys(body).length, 2); - assert.ok(body.event.hasOwnProperty("message")); - assert.strictEqual(body.event.message, context.message); - assert.strictEqual(body.event.severity, "info"); + var event = body.event; + assert.ok(event.hasOwnProperty("message")); + assert.strictEqual(event.message.length, context.message.length); + assert.strictEqual(event.message[0], context.message[0]); + assert.strictEqual(event.severity, "info"); }); it("should objectify data as object, without severity param", function() { var context = { @@ -108,43 +90,50 @@ describe("SplunkLogger _makedata", function() { prop: "something" } }; - var body = SplunkLogger.prototype._makeBody(context); + var logger = new SplunkLogger({token: "token-goes-here"}); + var body = logger._makeBody(context); assert.ok(body); assert.ok(body.hasOwnProperty("event")); assert.strictEqual(Object.keys(body).length, 2); - assert.ok(body.event.hasOwnProperty("message")); - assert.strictEqual(body.event.message, context.message); - assert.strictEqual(body.event.message.prop, "something"); - assert.strictEqual(body.event.severity, "info"); + var event = body.event; + assert.ok(event.hasOwnProperty("message")); + assert.strictEqual(Object.keys(event.message).length, Object.keys(context.message).length); + assert.strictEqual(event.message.prop, "something"); + assert.strictEqual(event.severity, "info"); }); it("should objectify data as string, with severity param", function() { var context = { message: "something", severity: "urgent" }; - var body = SplunkLogger.prototype._makeBody(context); + var logger = new SplunkLogger({token: "token-goes-here"}); + var body = logger._makeBody(context); assert.ok(body); assert.ok(body.hasOwnProperty("event")); assert.strictEqual(Object.keys(body).length, 2); - assert.ok(body.event.hasOwnProperty("message")); - assert.strictEqual(body.event.message, context.message); - assert.strictEqual(body.event.severity, "urgent"); + var event = body.event; + assert.ok(event.hasOwnProperty("message")); + assert.strictEqual(event.message, context.message); + assert.strictEqual(event.severity, "urgent"); }); it("should objectify data as array, with severity param", function() { var context = { message: ["something"], severity: "urgent" }; - var body = SplunkLogger.prototype._makeBody(context); + var logger = new SplunkLogger({token: "token-goes-here"}); + var body = logger._makeBody(context); assert.ok(body); assert.ok(body.hasOwnProperty("event")); assert.strictEqual(Object.keys(body).length, 2); - assert.ok(body.event.hasOwnProperty("message")); - assert.strictEqual(body.event.message, context.message); - assert.strictEqual(body.event.severity, "urgent"); + var event = body.event; + assert.ok(event.hasOwnProperty("message")); + assert.strictEqual(event.message.length, context.message.length); + assert.strictEqual(event.message[0], context.message[0]); + assert.strictEqual(event.severity, "urgent"); }); it("should objectify data as object, with severity param", function() { var context = { @@ -153,19 +142,21 @@ describe("SplunkLogger _makedata", function() { }, severity: "urgent" }; - var body = SplunkLogger.prototype._makeBody(context); + var logger = new SplunkLogger({token: "token-goes-here"}); + var body = logger._makeBody(context); assert.ok(body); assert.ok(body.hasOwnProperty("event")); assert.strictEqual(Object.keys(body).length, 2); - assert.ok(body.event.hasOwnProperty("message")); - assert.strictEqual(body.event.message, context.message); - assert.strictEqual(body.event.message.prop, "something"); - assert.strictEqual(body.event.severity, "urgent"); + var event = body.event; + assert.ok(event.hasOwnProperty("message")); + assert.strictEqual(Object.keys(event.message).length, Object.keys(context.message).length); + assert.strictEqual(event.message.prop, "something"); + assert.strictEqual(event.severity, "urgent"); }); }); -describe("SplunkLogger send", function() { - describe("using default middleware (integration tests)", function () { +describe("SplunkLogger send (integration tests)", function() { + describe("normal", function () { it("should error with bad token", function(done) { var config = { token: "token-goes-here" @@ -175,7 +166,6 @@ describe("SplunkLogger send", function() { var data = "something"; var context = { - config: config, message: data }; @@ -186,8 +176,15 @@ describe("SplunkLogger send", function() { assert.ok(err); assert.strictEqual(err.message, invalidTokenBody.text); assert.strictEqual(err.code, invalidTokenBody.code); + assert.ok(errContext); - assert.strictEqual(errContext, context); + var body = JSON.parse(errContext.message); + assert.ok(body.hasOwnProperty("time")); + assert.ok(body.hasOwnProperty("event")); + assert.strictEqual(Object.keys(body).length, 2); + var event = body.event; + assert.strictEqual(event.message, context.message); + assert.strictEqual(event.severity, "info"); }; logger.send(context, function(err, resp, body) { @@ -210,14 +207,15 @@ describe("SplunkLogger send", function() { var data = "something"; var context = { - config: config, message: data }; - assert.strictEqual(logger.contextQueue.length, 0); + assert.strictEqual(logger.serializedContextQueue.length, 0); + assert.strictEqual(logger.eventsBatchSize, 0); logger.send(context); setTimeout(function() { - assert.strictEqual(logger.contextQueue.length, 0); + assert.strictEqual(logger.serializedContextQueue.length, 0); + assert.strictEqual(logger.eventsBatchSize, 0); done(); }, 500); }); @@ -231,7 +229,6 @@ describe("SplunkLogger send", function() { var data = "something"; var context = { - config: config, message: data }; @@ -254,7 +251,6 @@ describe("SplunkLogger send", function() { var data = "something else"; var context = { - config: config, message: data, metadata: { time: new Date("January 1, 2015") @@ -281,7 +277,6 @@ describe("SplunkLogger send", function() { var data = "something else"; var context = { - config: config, message: data, metadata: { index: "default" @@ -311,7 +306,6 @@ describe("SplunkLogger send", function() { var data = "something else"; var context = { - config: config, message: data, metadata: { source: "_____new____source" @@ -337,7 +331,6 @@ describe("SplunkLogger send", function() { var data = "something else"; var context = { - config: config, message: data, metadata: { sourcetype: "_____new____sourcetype" @@ -363,7 +356,6 @@ describe("SplunkLogger send", function() { var data = "something else"; var context = { - config: logger.config, message: data, metadata: { host: "some.other.host" @@ -379,18 +371,16 @@ describe("SplunkLogger send", function() { done(); }); }); - it("should succeed with different valid token passed through context", function(done) { + it("should succeed with valid token", function(done) { var config = { - token: "invalid-token" + token: configurationFile.token }; var logger = new SplunkLogger(config); var data = "something"; - config.token = configurationFile.token; var context = { - config: config, message: data }; @@ -403,17 +393,18 @@ describe("SplunkLogger send", function() { done(); }); }); - it("should succeed with valid token", function(done) { + it("should succeed without token passed through context", function(done) { var config = { token: configurationFile.token }; - var logger = new SplunkLogger(config); + assert.strictEqual(logger.config.token, config.token); + var data = "something"; var context = { - config: config, + config: {}, message: data }; @@ -426,41 +417,57 @@ describe("SplunkLogger send", function() { done(); }); }); - it("should succeed without token passed through context", function(done) { + it("should fail on wrong protocol (assumes HTTP is invalid)", function(done) { var config = { - token: configurationFile.token + token: configurationFile.token, + protocol: "http" }; - var logger = new SplunkLogger(config); - assert.strictEqual(logger.config.token, config.token); + var logger = new SplunkLogger(config); var data = "something"; - var context = { - config: {}, message: data }; + var run = false; + + logger.error = function(err, errContext) { + run = true; + assert.ok(err); + assert.strictEqual(err.message, "socket hang up"); + assert.strictEqual(err.code, "ECONNRESET"); + + assert.ok(errContext); + var body = JSON.parse(errContext.message); + assert.ok(body.hasOwnProperty("time")); + assert.ok(body.hasOwnProperty("event")); + assert.strictEqual(Object.keys(body).length, 2); + var event = body.event; + assert.strictEqual(event.message, context.message); + assert.strictEqual(event.severity, "info"); + }; + logger.send(context, function(err, resp, body) { - assert.ok(!err); - assert.strictEqual(resp.headers["content-type"], "application/json; charset=UTF-8"); - assert.strictEqual(resp.body, body); - assert.strictEqual(body.text, successBody.text); - assert.strictEqual(body.code, successBody.code); + assert.ok(err); + assert.ok(run); + assert.strictEqual(err.message, "socket hang up"); + assert.strictEqual(err.code, "ECONNRESET"); + assert.ok(!resp); + assert.ok(!body); done(); }); }); - it("should fail on wrong protocol (assumes HTTP is invalid)", function(done) { + it("should fail on wrong Splunk server", function(done) { var config = { token: configurationFile.token, - protocol: "http" + url: "https://something-so-invalid-that-it-should-never-exist.xyz:12345/junk" }; var logger = new SplunkLogger(config); var data = "something"; var context = { - config: config, message: data }; @@ -469,17 +476,24 @@ describe("SplunkLogger send", function() { logger.error = function(err, errContext) { run = true; assert.ok(err); - assert.strictEqual(err.message, "socket hang up"); - assert.strictEqual(err.code, "ECONNRESET"); + assert.strictEqual(err.message, "getaddrinfo ENOTFOUND"); + assert.strictEqual(err.code, "ENOTFOUND"); + assert.ok(errContext); - assert.strictEqual(errContext, context); + var body = JSON.parse(errContext.message); + assert.ok(body.hasOwnProperty("time")); + assert.ok(body.hasOwnProperty("event")); + assert.strictEqual(Object.keys(body).length, 2); + var event = body.event; + assert.strictEqual(event.message, context.message); + assert.strictEqual(event.severity, "info"); }; logger.send(context, function(err, resp, body) { assert.ok(err); assert.ok(run); - assert.strictEqual(err.message, "socket hang up"); - assert.strictEqual(err.code, "ECONNRESET"); + assert.strictEqual(err.message, "getaddrinfo ENOTFOUND"); + assert.strictEqual(err.code, "ENOTFOUND"); assert.ok(!resp); assert.ok(!body); done(); @@ -495,7 +509,6 @@ describe("SplunkLogger send", function() { var data = "something"; var context = { - config: config, message: data }; @@ -510,18 +523,16 @@ describe("SplunkLogger send", function() { }); it("should error with valid token, using strict SSL", function(done) { var config = { - token: configurationFile.token, + token: configurationFile.token }; var logger = new SplunkLogger(config); + logger.requestOptions.strictSSL = true; + var data = "something"; var context = { - config: config, - message: data, - requestOptions: { - strictSSL: true - } + message: data }; var run = false; @@ -531,7 +542,16 @@ describe("SplunkLogger send", function() { assert.ok(err); assert.strictEqual(err.message, "SELF_SIGNED_CERT_IN_CHAIN"); assert.ok(errContext); - assert.strictEqual(errContext, context); + + var body = JSON.parse(errContext.message); + assert.ok(body.hasOwnProperty("time")); + assert.ok(body.hasOwnProperty("event")); + var event = body.event; + assert.ok(event.hasOwnProperty("message")); + assert.ok(event.hasOwnProperty("severity")); + + assert.strictEqual(event.message, context.message); + assert.strictEqual(event.severity, "info"); }; logger.send(context, function(err, resp, body) { @@ -552,33 +572,36 @@ describe("SplunkLogger send", function() { var data = "batched event"; var context = { - config: config, message: data }; var sent = 0; // Wrap sendevents to ensure it gets called + var sendEvents = logger._sendEvents; logger._sendEvents = function(cont, cb) { sent++; - SplunkLogger.prototype._sendEvents(cont, cb); + sendEvents(cont, cb); }; logger.send(context); - logger.send(context); + var context2 = { + message: "second batched event" + }; + logger.send(context2); setTimeout(function() { - assert.strictEqual(logger.contextQueue.length, 0); + assert.strictEqual(logger.serializedContextQueue.length, 0); + assert.strictEqual(logger.eventsBatchSize, 0); assert.strictEqual(sent, 2); done(); }, 1000); }); }); - describe("without autoFlush (integration tests)", function () { + describe("default batching settings", function () { it("should get no data response when flushing empty batch with valid token", function(done) { var config = { - token: configurationFile.token, - autoFlush: false + token: configurationFile.token }; var logger = new SplunkLogger(config); @@ -593,7 +616,8 @@ describe("SplunkLogger send", function() { assert.ok(errContext); }; - assert.strictEqual(logger.contextQueue.length, 0); + assert.strictEqual(logger.serializedContextQueue.length, 0); + assert.strictEqual(logger.eventsBatchSize, 0); logger.flush(function(err, resp, body) { assert.ok(!err); assert.ok(run); @@ -601,14 +625,14 @@ describe("SplunkLogger send", function() { assert.strictEqual(resp.body, body); assert.strictEqual(body.text, noDataBody.text); assert.strictEqual(body.code, noDataBody.code); - assert.strictEqual(logger.contextQueue.length, 0); + assert.strictEqual(logger.serializedContextQueue.length, 0); + assert.strictEqual(logger.eventsBatchSize, 0); done(); }); }); it("should be noop when flushing empty batch, without callback, with valid token", function(done) { var config = { - token: configurationFile.token, - autoFlush: false + token: configurationFile.token }; var logger = new SplunkLogger(config); @@ -622,518 +646,1212 @@ describe("SplunkLogger send", function() { }; // Nothing should be sent if queue is empty - assert.strictEqual(logger.contextQueue.length, 0); + assert.strictEqual(logger.serializedContextQueue.length, 0); + assert.strictEqual(logger.eventsBatchSize, 0); logger.flush(); - assert.strictEqual(logger.contextQueue.length, 0); + assert.strictEqual(logger.serializedContextQueue.length, 0); + assert.strictEqual(logger.eventsBatchSize, 0); }); it("should flush a batch of 1 event with valid token", function(done) { var config = { token: configurationFile.token, - autoFlush: false + maxBatchCount: 0 // Use manual batching }; var logger = new SplunkLogger(config); - var data = "batched event 1"; + var data = this.test.fullTitle(); var context = { - config: config, message: data }; logger.send(context); - assert.strictEqual(logger.contextQueue.length, 1); + assert.strictEqual(logger.serializedContextQueue.length, 1); + assert.ok(logger.eventsBatchSize > 50); logger.flush(function(err, resp, body) { assert.ok(!err); assert.strictEqual(resp.headers["content-type"], "application/json; charset=UTF-8"); assert.strictEqual(resp.body, body); assert.strictEqual(body.text, successBody.text); assert.strictEqual(body.code, successBody.code); - assert.strictEqual(logger.contextQueue.length, 0); + assert.strictEqual(logger.serializedContextQueue.length, 0); + assert.strictEqual(logger.eventsBatchSize, 0); done(); }); }); it("should flush a batch of 2 events with valid token", function(done) { var config = { token: configurationFile.token, - autoFlush: false + maxBatchCount: 0 }; var logger = new SplunkLogger(config); - var data = "batched event"; + var data = this.test.fullTitle(); var context = { - config: config, message: data }; logger.send(context); logger.send(context); - assert.strictEqual(logger.contextQueue.length, 2); + assert.strictEqual(logger.serializedContextQueue.length, 2); + assert.ok(logger.eventsBatchSize > 100); logger.flush(function(err, resp, body) { assert.ok(!err); assert.strictEqual(resp.headers["content-type"], "application/json; charset=UTF-8"); assert.strictEqual(resp.body, body); assert.strictEqual(body.text, successBody.text); assert.strictEqual(body.code, successBody.code); - assert.strictEqual(logger.contextQueue.length, 0); + assert.strictEqual(logger.serializedContextQueue.length, 0); + assert.strictEqual(logger.eventsBatchSize, 0); done(); }); }); }); - describe("using custom middleware", function() { - it("should error with non-function middleware", function() { + describe("using retry", function() { + it("should retry exactly 0 times (send once only)", function(done) { var config = { - token: "a-token-goes-here-usually" + token: configurationFile.token, + maxRetries: 0 }; - try { - var logger = new SplunkLogger(config); - logger.use("not a function"); - assert.fail(false, "Expected an error."); - } - catch (err) { - assert.ok(err); - assert.strictEqual(err.message, "Middleware must be a function."); - } + var logger = new SplunkLogger(config); + + var retryCount = 0; + + // Wrap the _post so we can verify retries + var post = logger._post; + logger._post = function(requestOptions, callback) { + retryCount++; + if (retryCount === config.maxRetries + 1) { + post(requestOptions, callback); + } + else { + callback(new Error(), {body: invalidTokenBody}, invalidTokenBody); + } + }; + + var payload = { + message: "something" + }; + logger.send(payload, function(err, resp, body) { + assert.ok(!err); + assert.ok(resp); + assert.ok(body); + assert.strictEqual(body.code, successBody.code); + assert.strictEqual(body.text, successBody.text); + assert.strictEqual(retryCount, config.maxRetries + 1); + done(); + }); }); - it("should succeed using non-default middleware", function(done) { + it("should retry exactly once", function(done) { var config = { - token: "token-goes-here" + token: configurationFile.token, + maxRetries: 1, + maxBatchCount: 1 }; + var logger = new SplunkLogger(config); - var middlewareCount = 0; - - function middleware(context, next) { - middlewareCount++; - assert.strictEqual(context.message, "something"); - next(null, context); - } + var retryCount = 0; - var logger = new SplunkLogger(config); - logger.use(middleware); - - logger._sendEvents = function(context, next) { - var response = { - headers: { - "content-type": "application/json; charset=UTF-8", - isCustom: true - }, - body: successBody - }; - next(null, response, successBody); + // Wrap the _post so we can verify retries + var post = logger._post; + logger._post = function(requestOptions, callback) { + retryCount++; + if (retryCount === config.maxRetries + 1) { + post(requestOptions, callback); + } + else { + callback(new Error(), {body: invalidTokenBody}, invalidTokenBody); + } }; - - var initialData = "something"; - var context = { - config: config, - message: initialData + + var payload = { + message: "something" }; - - logger.send(context, function(err, resp, body) { + logger.send(payload, function(err, resp, body) { assert.ok(!err); - assert.strictEqual(resp.body, body); + assert.ok(resp); + assert.ok(body); assert.strictEqual(body.code, successBody.code); assert.strictEqual(body.text, successBody.text); - assert.strictEqual(middlewareCount, 1); + assert.strictEqual(retryCount, config.maxRetries + 1); done(); }); }); - it("should succeed using non-default middleware, without passing the context through", function(done) { + it("should retry exactly twice", function(done) { var config = { - token: "token-goes-here" + token: configurationFile.token, + maxRetries: 2, + maxBatchCount: 1 }; + var logger = new SplunkLogger(config); - var middlewareCount = 0; - - function middleware(context, next) { - middlewareCount++; - assert.strictEqual(context.message, "something"); - next(null); - } + var retryCount = 0; - var logger = new SplunkLogger(config); - logger.use(middleware); - - logger._sendEvents = function(context, next) { - assert.strictEqual(context, initialContext); - var response = { - headers: { - "content-type": "application/json; charset=UTF-8", - isCustom: true - }, - body: successBody - }; - next(null, response, successBody); + // Wrap the _post so we can verify retries + var post = logger._post; + logger._post = function(requestOptions, callback) { + retryCount++; + if (retryCount === config.maxRetries + 1) { + post(requestOptions, callback); + } + else { + callback(new Error(), {body: invalidTokenBody}, invalidTokenBody); + } }; - - var initialData = "something"; - var initialContext = { - config: config, - message: initialData + + var payload = { + message: "something" }; - - logger.send(initialContext, function(err, resp, body) { + logger.send(payload, function(err, resp, body) { assert.ok(!err); - assert.strictEqual(resp.body, body); + assert.ok(resp); + assert.ok(body); assert.strictEqual(body.code, successBody.code); assert.strictEqual(body.text, successBody.text); - assert.strictEqual(middlewareCount, 1); + assert.strictEqual(retryCount, config.maxRetries + 1); done(); }); }); - it("should succeed using 2 middlewares", function(done) { + it("should retry exactly 5 times", function(done) { var config = { - token: "token-goes-here" + token: configurationFile.token, + maxRetries: 5, + maxBatchCount: 1 }; + var logger = new SplunkLogger(config); - var middlewareCount = 0; - - function middleware(context, callback) { - middlewareCount++; - assert.strictEqual(context.message, "somet??hing"); - context.message = encodeURIComponent(context.message); - callback(null, context); - } - - function middleware2(context, callback) { - middlewareCount++; - assert.strictEqual(context.message, "somet%3F%3Fhing"); - callback(null, context); - } + var retryCount = 0; - var logger = new SplunkLogger(config); - logger.use(middleware); - logger.use(middleware2); - - logger._sendEvents = function(context, next) { - assert.strictEqual(context.message, "somet%3F%3Fhing"); - var response = { - headers: { - "content-type": "application/json; charset=UTF-8", - isCustom: true - }, - body: successBody - }; - next(null, response, successBody); + // Wrap the _post so we can verify retries + var post = logger._post; + logger._post = function(requestOptions, callback) { + retryCount++; + if (retryCount === config.maxRetries + 1) { + post(requestOptions, callback); + } + else { + callback(new Error(), {body: invalidTokenBody}, invalidTokenBody); + } }; - - var initialData = "somet??hing"; - var context = { - config: config, - message: initialData + + var payload = { + message: "something" }; - - logger.send(context, function(err, resp, body) { + logger.send(payload, function(err, resp, body) { assert.ok(!err); - assert.strictEqual(resp.body, body); + assert.ok(resp); + assert.ok(body); assert.strictEqual(body.code, successBody.code); assert.strictEqual(body.text, successBody.text); - assert.strictEqual(middlewareCount, 2); + assert.strictEqual(retryCount, config.maxRetries + 1); done(); }); }); - it("should succeed using 3 middlewares", function(done) { + it("should not retry on initial success when maxRetries=1", function(done) { var config = { - token: "token-goes-here" + token: configurationFile.token, + maxRetries: 1, + maxBatchCount: 1 }; + var logger = new SplunkLogger(config); - var middlewareCount = 0; - - function middleware(context, next) { - middlewareCount++; - assert.strictEqual(context.message, "somet??hing"); - context.message = encodeURIComponent(context.message); - next(null, context); - } - - function middleware2(context, next) { - middlewareCount++; - assert.strictEqual(context.message, "somet%3F%3Fhing"); - context.message = decodeURIComponent(context.message) + " changed"; - assert.strictEqual(context.message, "somet??hing changed"); - next(null, context); - } - - function middleware3(context, next) { - middlewareCount++; - assert.strictEqual(context.message, "somet??hing changed"); - next(null, context); - } + var retryCount = 0; - var logger = new SplunkLogger(config); - logger.use(middleware); - logger.use(middleware2); - logger.use(middleware3); - - logger._sendEvents = function(context, next) { - assert.strictEqual(context.message, "somet??hing changed"); - var response = { - headers: { - "content-type": "application/json; charset=UTF-8", - isCustom: true - }, - body: successBody - }; - next(null, response, successBody); + // Wrap the _post so we can verify retries + var post = logger._post; + logger._post = function(requestOptions, callback) { + retryCount++; + if (retryCount === 1) { + post(requestOptions, callback); + } + else { + callback(new Error(), {body: invalidTokenBody}, invalidTokenBody); + } }; - - var initialData = "somet??hing"; - var context = { - config: config, - message: initialData + + var payload = { + message: "something" }; - - logger.send(context, function(err, resp, body) { + logger.send(payload, function(err, resp, body) { assert.ok(!err); - assert.strictEqual(resp.body, body); + assert.ok(resp); + assert.ok(body); assert.strictEqual(body.code, successBody.code); assert.strictEqual(body.text, successBody.text); - assert.strictEqual(middlewareCount, 3); + assert.strictEqual(retryCount, 1); done(); }); }); - it("should succeed using 3 middlewares with data object", function(done) { + it("should retry once when maxRetries=10", function(done) { var config = { - token: "token-goes-here" + token: configurationFile.token, + maxRetries: 10, + maxBatchCount: 1 }; - - var middlewareCount = 0; - - function middleware(context, next) { - middlewareCount++; - assert.strictEqual(context.message, initialData); - - assert.strictEqual(context.message.property, initialData.property); - assert.strictEqual(context.message.nested.object, initialData.nested.object); - assert.strictEqual(context.message.number, initialData.number); - assert.strictEqual(context.message.bool, initialData.bool); - - context.message.property = "new"; - context.message.bool = true; - next(null, context); - } - - function middleware2(context, next) { - middlewareCount++; - - assert.strictEqual(context.message.property, "new"); - assert.strictEqual(context.message.nested.object, initialData.nested.object); - assert.strictEqual(context.message.number, initialData.number); - assert.strictEqual(context.message.bool, true); - - context.message.number = 789; - next(null, context); - } - - function middleware3(context, next) { - middlewareCount++; - - assert.strictEqual(context.message.property, "new"); - assert.strictEqual(context.message.nested.object, initialData.nested.object); - assert.strictEqual(context.message.number, 789); - assert.strictEqual(context.message.bool, true); - - next(null, context); - } - var logger = new SplunkLogger(config); - logger.use(middleware); - logger.use(middleware2); - logger.use(middleware3); - - logger._sendEvents = function(context, next) { - assert.strictEqual(context.message.property, "new"); - assert.strictEqual(context.message.nested.object, initialData.nested.object); - assert.strictEqual(context.message.number, 789); - assert.strictEqual(context.message.bool, true); - - var response = { - headers: { - "content-type": "application/json; charset=UTF-8", - isCustom: true - }, - body: successBody - }; - next(null, response, successBody); - }; - var initialData = { - property: "one", - nested: { - object: "value" - }, - number: 1234, - bool: false + var retryCount = 0; + + // Wrap the _post so we can verify retries + var post = logger._post; + logger._post = function(requestOptions, callback) { + retryCount++; + if (retryCount === 2) { + post(requestOptions, callback); + } + else { + callback(new Error(), {body: invalidTokenBody}, invalidTokenBody); + } }; - var context = { - config: config, - message: initialData + + var payload = { + message: "something" }; - - logger.send(context, function(err, resp, body) { + logger.send(payload, function(err, resp, body) { assert.ok(!err); - assert.strictEqual(resp.body, body); + assert.ok(resp); + assert.ok(body); assert.strictEqual(body.code, successBody.code); assert.strictEqual(body.text, successBody.text); - assert.strictEqual(middlewareCount, 3); + assert.strictEqual(retryCount, 2); done(); }); }); - }); - describe("error handlers", function() { - it("should get error and context using default error handler, without passing context to next()", function(done) { + it("should retry on request error when maxRetries=0", function(done) { var config = { - token: "token-goes-here" + token: configurationFile.token, + maxRetries: 0, + host: "bad-hostname.invalid", + maxBatchCount: 1 }; - - var middlewareCount = 0; - - function middleware(context, next) { - middlewareCount++; - assert.strictEqual(context.message, "something"); - context.message = "something else"; - next(new Error("error!")); - } - var logger = new SplunkLogger(config); - logger.use(middleware); - var initialData = "something"; - var initialContext = { - config: config, - message: initialData + var retryCount = 0; + + // Wrap the _post so we can verify retries + var post = logger._post; + logger._post = function(requestOptions, callback) { + retryCount++; + post(requestOptions, callback); }; var run = false; - - // Wrap the default error callback for code coverage - var errCallback = logger.error; logger.error = function(err, context) { - run = true; assert.ok(err); assert.ok(context); - assert.strictEqual(err.message, "error!"); - initialContext.message = "something else"; - assert.strictEqual(context, initialContext); + run = true; + }; + + var payload = { + message: "something" + }; + logger.send(payload, function(err, resp, body) { + assert.ok(err); + assert.ok(!resp); + assert.ok(!body); - mute(); - errCallback(err, context); - unmute(); - + assert.strictEqual(config.maxRetries + 1, retryCount); + assert.ok(run); done(); - }; - - // Fire & forget, the callback won't be called anyways due to the error - logger.send(initialContext); - - assert.ok(run); - assert.strictEqual(middlewareCount, 1); + }); }); - it("should get error and context using default error handler", function(done) { + it("should retry on request error when maxRetries=1", function(done) { var config = { - token: "token-goes-here" + token: configurationFile.token, + maxRetries: 1, + host: "bad-hostname.invalid", + maxBatchCount: 1 }; - - var middlewareCount = 0; - - function middleware(context, next) { - middlewareCount++; - assert.strictEqual(context.message, "something"); - context.message = "something else"; - next(new Error("error!"), context); - } - var logger = new SplunkLogger(config); - logger.use(middleware); - var initialData = "something"; - var initialContext = { - config: config, - message: initialData + var retryCount = 0; + + // Wrap the _post so we can verify retries + var post = logger._post; + logger._post = function(requestOptions, callback) { + retryCount++; + post(requestOptions, callback); }; var run = false; - - // Wrap the default error callback for code coverage - var errCallback = logger.error; logger.error = function(err, context) { - run = true; assert.ok(err); assert.ok(context); - assert.strictEqual(err.message, "error!"); - initialContext.message = "something else"; - assert.strictEqual(context, initialContext); - - mute(); - errCallback(err, context); - unmute(); - - done(); + run = true; }; - - // Fire & forget, the callback won't be called anyways due to the error - logger.send(initialContext); - - assert.ok(run); - assert.strictEqual(middlewareCount, 1); + + var payload = { + message: "something" + }; + logger.send(payload, function(err, resp, body) { + assert.ok(err); + assert.ok(!resp); + assert.ok(!body); + + assert.strictEqual(config.maxRetries + 1, retryCount); + assert.ok(run); + done(); + }); }); - it("should get error and context sending twice using default error handler", function(done) { + it("should retry on request error when maxRetries=5", function(done) { var config = { - token: "token-goes-here" + token: configurationFile.token, + maxRetries: 5, + host: "bad-hostname.invalid", + maxBatchCount: 1 }; - - var middlewareCount = 0; - - function middleware(context, next) { - middlewareCount++; - assert.strictEqual(context.message, "something"); - context.message = "something else"; - next(new Error("error!"), context); - } - var logger = new SplunkLogger(config); - logger.use(middleware); - var initialData = "something"; - var context1 = { - config: config, - message: initialData + var retryCount = 0; + + // Wrap the _post so we can verify retries + var post = logger._post; + logger._post = function(requestOptions, callback) { + retryCount++; + post(requestOptions, callback); }; - // Wrap the default error callback for code coverage - var errCallback = logger.error; + var run = false; logger.error = function(err, context) { assert.ok(err); - assert.strictEqual(err.message, "error!"); assert.ok(context); - assert.strictEqual(context.message, "something else"); - - var comparing = context1; - if (middlewareCount === 2) { - comparing = context2; - } - assert.strictEqual(context.severity, comparing.severity); - assert.strictEqual(context.config, comparing.config); - assert.strictEqual(context.requestOptions, comparing.requestOptions); + run = true; + }; + + var payload = { + message: "something" + }; + logger.send(payload, function(err, resp, body) { + assert.ok(err); + assert.ok(!resp); + assert.ok(!body); - mute(); - errCallback(err, context); - unmute(); - - if (middlewareCount === 2) { - done(); - } + assert.strictEqual(config.maxRetries + 1, retryCount); + assert.ok(run); + done(); + }); + }); + it("should not retry on Splunk error when maxRetries=0", function(done) { + var config = { + token: "invalid-token", + maxRetries: 0, + maxBatchCount: 1 }; + var logger = new SplunkLogger(config); - // Fire & forget, the callback won't be called anyways due to the error - logger.send(context1); - // Reset the data, hopefully this doesn't explode - var context2 = JSON.parse(JSON.stringify(context1)); - context2.message = "something"; - logger.send(context2); + var retryCount = 0; - assert.strictEqual(middlewareCount, 2); + // Wrap the _post so we can verify retries + var post = logger._post; + logger._post = function(requestOptions, callback) { + retryCount++; + post(requestOptions, callback); + }; + + var run = false; + logger.error = function(err, context) { + assert.ok(err); + assert.ok(context); + run = true; + }; + + var payload = { + message: "something" + }; + logger.send(payload, function(err, resp, body) { + assert.ok(!err); + assert.ok(resp); + assert.ok(body); + assert.strictEqual(invalidTokenBody.code, body.code); + assert.strictEqual(invalidTokenBody.text, body.text); + + assert.strictEqual(1, retryCount); + assert.ok(run); + done(); + }); + }); + it("should not retry on Splunk error when maxRetries=1", function(done) { + var config = { + token: "invalid-token", + maxRetries: 1, + maxBatchCount: 1 + }; + var logger = new SplunkLogger(config); + + var retryCount = 0; + + // Wrap the _post so we can verify retries + var post = logger._post; + logger._post = function(requestOptions, callback) { + retryCount++; + post(requestOptions, callback); + }; + + var run = false; + logger.error = function(err, context) { + assert.ok(err); + assert.ok(context); + run = true; + }; + + var payload = { + message: "something" + }; + logger.send(payload, function(err, resp, body) { + assert.ok(!err); + assert.ok(resp); + assert.ok(body); + assert.strictEqual(invalidTokenBody.code, body.code); + assert.strictEqual(invalidTokenBody.text, body.text); + + assert.strictEqual(1, retryCount); + assert.ok(run); + done(); + }); + }); + it("should not retry on Splunk error when maxRetries=5", function(done) { + var config = { + token: "invalid-token", + maxRetries: 5, + maxBatchCount: 1 + }; + var logger = new SplunkLogger(config); + + var retryCount = 0; + + // Wrap the _post so we can verify retries + var post = logger._post; + logger._post = function(requestOptions, callback) { + retryCount++; + post(requestOptions, callback); + }; + + var run = false; + logger.error = function(err, context) { + assert.ok(err); + assert.ok(context); + run = true; + }; + + var payload = { + message: "something" + }; + logger.send(payload, function(err, resp, body) { + assert.ok(!err); + assert.ok(resp); + assert.ok(body); + assert.strictEqual(body.code, invalidTokenBody.code); + assert.strictEqual(body.text, invalidTokenBody.text); + + assert.strictEqual(1, retryCount); + assert.ok(run); + done(); + }); + }); + }); + describe("using batch interval", function() { + it("should not make a POST request if contextQueue is always empty", function(done) { + var config = { + token: configurationFile.token, + batchInterval: 100 + }; + var logger = new SplunkLogger(config); + + var posts = 0; + + // Wrap _post so we can verify how many times we called it + var _post = logger._post; + logger._post = function(context, callback) { + _post(context, function(err, resp, body) { + posts++; + assert.ok(!err); + assert.strictEqual(body.code, successBody.code); + assert.strictEqual(body.text, successBody.text); + callback(err, resp, body); + }); + }; + + setTimeout(function() { + assert.strictEqual(logger._timerDuration, 100); + assert.strictEqual(posts, 0); + assert.strictEqual(logger.serializedContextQueue.length, 0); + assert.strictEqual(logger.eventsBatchSize, 0); + + // Clean up the timer + logger._disableTimer(); + done(); + }, 500); + }); + it("should only make 1 POST request for 1 event", function(done) { + var config = { + token: configurationFile.token, + batchInterval: 100, + maxBatchCount: 10 + }; + var logger = new SplunkLogger(config); + + var posts = 0; + + // Wrap _post so we can verify how many times we called it + var _post = logger._post; + logger._post = function(context, callback) { + _post(context, function(err, resp, body) { + posts++; + assert.ok(!err); + assert.strictEqual(body.code, successBody.code); + assert.strictEqual(body.text, successBody.text); + callback(err, resp, body); + }); + }; + + var payload = { + message: "something" + }; + logger.send(payload); + + setTimeout(function() { + assert.strictEqual(logger._timerDuration, 100); + assert.strictEqual(posts, 1); + assert.strictEqual(logger.serializedContextQueue.length, 0); + assert.strictEqual(logger.eventsBatchSize, 0); + + // Clean up the timer + logger._disableTimer(); + done(); + }, 500); + }); + it("should only make 1 POST request for 2 events", function(done) { + var config = { + token: configurationFile.token, + batchInterval: 100, + maxBatchCount: 10 + }; + var logger = new SplunkLogger(config); + + var posts = 0; + + // Wrap _post so we can verify how many times we called it + var _post = logger._post; + logger._post = function(context, callback) { + _post(context, function(err, resp, body) { + posts++; + assert.ok(!err); + assert.strictEqual(body.code, successBody.code); + assert.strictEqual(body.text, successBody.text); + callback(err, resp, body); + }); + }; + + var payload = { + message: "something" + }; + var payload2 = { + message: "something 2" + }; + logger.send(payload); + logger.send(payload2); + + setTimeout(function() { + assert.strictEqual(logger._timerDuration, 100); + assert.strictEqual(posts, 1); + assert.strictEqual(logger.serializedContextQueue.length, 0); + + // Clean up the timer + logger._disableTimer(); + done(); + }, 500); + }); + it("should only make 1 POST request for 5 events", function(done) { + var config = { + token: configurationFile.token, + batchInterval: 200, + maxBatchSize: 5000, + maxBatchCount: 10 + }; + var logger = new SplunkLogger(config); + + var posts = 0; + + // Wrap _post so we can verify how many times we called it + var _post = logger._post; + logger._post = function(context, callback) { + _post(context, function(err, resp, body) { + posts++; + assert.ok(!err); + assert.strictEqual(body.code, successBody.code); + assert.strictEqual(body.text, successBody.text); + callback(err, resp, body); + }); + }; + + var payload = { + message: "something" + }; + logger.send(payload); + logger.send(payload); + logger.send(payload); + logger.send(payload); + logger.send(payload); + + setTimeout(function() { + assert.strictEqual(logger._timerDuration, 200); + assert.strictEqual(posts, 1); + assert.strictEqual(logger.serializedContextQueue.length, 0); + assert.strictEqual(logger.eventsBatchSize, 0); + + // Clean up the timer + logger._disableTimer(); + done(); + }, 500); + }); + it("should error when trying to set batchInterval to a negative value after logger creation", function() { + var config = { + token: configurationFile.token + }; + var logger = new SplunkLogger(config); + + try { + logger._enableTimer(-1); + assert.ok(false, "Expected an error."); + } + catch(err) { + assert.ok(err); + assert.strictEqual(err.message, "Batch interval must be a positive number, found: -1"); + } + }); + it("should flush a stale event after enabling batching and batchInterval", function(done) { + var config = { + token: configurationFile.token, + maxBatchCount: 10 + }; + var logger = new SplunkLogger(config); + + var posts = 0; + + // Wrap _post so we can verify how many times we called it + var _post = logger._post; + logger._post = function(context, callback) { + _post(context, function(err, resp, body) { + posts++; + assert.ok(!err); + assert.strictEqual(body.code, successBody.code); + assert.strictEqual(body.text, successBody.text); + callback(err, resp, body); + }); + }; + + var payload = { + message: "something" + }; + logger.send(payload); + assert.strictEqual(logger._timerDuration, 0); + + logger.config.batchInterval = 100; + logger._initializeConfig(logger.config); + + var payload2 = { + message: "something else" + }; + logger.send(payload2); + + setTimeout(function() { + assert.strictEqual(posts, 1); + assert.strictEqual(logger.serializedContextQueue.length, 0); + assert.strictEqual(logger.eventsBatchSize, 0); + + // Clean up the timer + logger._disableTimer(); + done(); + }, 500); + }); + it("should flush an event with batchInterval, then set batchInterval=0 and maxBatchCount=3 for manual batching", function(done) { + var config = { + token: configurationFile.token, + batchInterval: 100, + maxBatchSize: 100000, + maxBatchCount: 3 + }; + var logger = new SplunkLogger(config); + + var posts = 0; + + // Wrap _post so we can verify how many times we called it + var _post = logger._post; + logger._post = function(context, callback) { + _post(context, function(err, resp, body) { + posts++; + assert.ok(!err); + assert.strictEqual(body.code, successBody.code); + assert.strictEqual(body.text, successBody.text); + callback(err, resp, body); + }); + }; + + var payload = { + message: "something" + }; + logger.send(payload); + + var run = false; + setTimeout(function() { + logger.config.batchInterval = 0; + + var payload2 = { + message: "something else" + }; + logger.send(payload2); + logger.send(payload2); + + assert.strictEqual(logger.serializedContextQueue.length, 2); + assert.ok(logger.eventsBatchSize > 150); + logger.send(payload2); // This should trigger a flush + run = true; + }, 150); + + setTimeout(function() { + assert.ok(run); + assert.strictEqual(posts, 2); + assert.strictEqual(logger.serializedContextQueue.length, 0); + assert.strictEqual(logger.eventsBatchSize, 0); + + // Clean up the timer + logger._disableTimer(); + done(); + }, 500); + }); + it("should flush an event with batchInterval=100", function(done) { + var config = { + token: configurationFile.token, + batchInterval: 100, + maxBatchCount: 10 + }; + var logger = new SplunkLogger(config); + + var posts = 0; + + // Wrap _post so we can verify how many times we called it + var _post = logger._post; + logger._post = function(context, callback) { + _post(context, function(err, resp, body) { + posts++; + assert.ok(!err); + assert.strictEqual(body.code, successBody.code); + assert.strictEqual(body.text, successBody.text); + callback(err, resp, body); + }); + }; + + var payload = { + message: "something" + }; + logger.send(payload); + + var run = false; + setTimeout(function() { + var payload2 = { + message: "something else" + }; + logger.send(payload2); + + assert.strictEqual(logger.serializedContextQueue.length, 1); + assert.ok(logger.eventsBatchSize > 50); + run = true; + }, 150); + + + setTimeout(function() { + assert.ok(run); + assert.strictEqual(posts, 2); + assert.strictEqual(logger.serializedContextQueue.length, 0); + assert.strictEqual(logger.eventsBatchSize, 0); + + // Clean up the timer + logger._disableTimer(); + done(); + }, 300); + }); + }); + describe("using max batch size", function() { + it("should flush first event immediately with maxBatchSize=1", function(done) { + var config = { + token: configurationFile.token + }; + var logger = new SplunkLogger(config); + + var posts = 0; + + // Wrap _post so we can verify how many times we called it + var _post = logger._post; + logger._post = function(context) { + _post(context, function(err, resp, body) { + posts++; + + assert.ok(!logger._timerID); + assert.strictEqual(posts, 1); + assert.strictEqual(logger.serializedContextQueue.length, 0); + assert.strictEqual(logger.eventsBatchSize, 0); + + assert.ok(!err); + assert.strictEqual(body.code, successBody.code); + assert.strictEqual(body.text, successBody.text); + + done(); + }); + }; + + var payload = { + message: "more than 1 byte" + }; + logger.send(payload); + }); + it("should flush first 2 events after maxBatchSize>100", function(done) { + var config = { + token: configurationFile.token, + maxBatchCount: 10, + maxBatchSize: 100 + }; + var logger = new SplunkLogger(config); + + var posts = 0; + + // Wrap _post so we can verify how many times we called it + var _post = logger._post; + logger._post = function(context) { + _post(context, function(err, resp, body) { + posts++; + + assert.ok(!logger._timerID); + assert.strictEqual(posts, 1); + assert.strictEqual(logger.serializedContextQueue.length, 0); + assert.strictEqual(logger.eventsBatchSize, 0); + + assert.ok(!err); + assert.strictEqual(body.code, successBody.code); + assert.strictEqual(body.text, successBody.text); + + done(); + }); + }; + + var payload = { + message: "more than 1 byte" + }; + logger.send(payload); + + setTimeout(function() { + assert.ok(!logger._timerID); + assert.strictEqual(posts, 0); + assert.strictEqual(logger.serializedContextQueue.length, 1); + assert.ok(logger.eventsBatchSize > 50); + + logger.send(payload); + }, 300); + + setTimeout(function() { + assert.ok(!logger._timerID); + assert.strictEqual(posts, 1); + assert.strictEqual(logger.serializedContextQueue.length, 0); + assert.strictEqual(logger.eventsBatchSize, 0); + }, 400); + }); + it("should flush first event after 200ms, with maxBatchSize=200", function(done) { + var config = { + token: configurationFile.token, + maxBatchSize: 200, + batchInterval: 200, + maxBatchCount: 10 + }; + var logger = new SplunkLogger(config); + + var posts = 0; + + // Wrap _post so we can verify how many times we called it + var _post = logger._post; + logger._post = function(context) { + _post(context, function(err, resp, body) { + posts++; + + assert.ok(!err); + assert.strictEqual(body.code, successBody.code); + assert.strictEqual(body.text, successBody.text); + }); + }; + + var payload = { + message: "more than 1 byte" + }; + logger.send(payload); + + // Make sure the event wasn't flushed yet + setTimeout(function() { + assert.strictEqual(logger.serializedContextQueue.length, 1); + assert.ok(logger.eventsBatchSize > 50); + }, 150); + + setTimeout(function() { + assert.ok(logger._timerID); + assert.strictEqual(logger._timerDuration, 200); + logger._disableTimer(); + + assert.strictEqual(posts, 1); + assert.strictEqual(logger.serializedContextQueue.length, 0); + assert.strictEqual(logger.eventsBatchSize, 0); + done(); + }, 250); + }); + it("should flush first event before 200ms, with maxBatchSize=1", function(done) { + var config = { + token: configurationFile.token, + maxBatchSize: 1, + batchInterval: 200, + maxBatchCount: 10 + }; + var logger = new SplunkLogger(config); + + var posts = 0; + + // Wrap _post so we can verify how many times we called it + var _post = logger._post; + logger._post = function(context) { + _post(context, function(err, resp, body) { + posts++; + + assert.ok(!err); + assert.strictEqual(body.code, successBody.code); + assert.strictEqual(body.text, successBody.text); + }); + }; + + var payload = { + message: "more than 1 byte" + }; + logger.send(payload); + + // Event should be sent before the interval timer runs the first time + setTimeout(function() { + assert.strictEqual(logger.serializedContextQueue.length, 0); + assert.strictEqual(posts, 1); + }, 150); + + setTimeout(function() { + assert.ok(logger._timerID); + assert.strictEqual(logger._timerDuration, 200); + logger._disableTimer(); + + assert.strictEqual(posts, 1); + assert.strictEqual(logger.serializedContextQueue.length, 0); + done(); + }, 250); + }); + }); + describe("using max batch count", function() { + it("should flush first event immediately with maxBatchCount=1 with large maxBatchSize", function(done) { + var config = { + token: configurationFile.token, + maxBatchCount: 1, + maxBatchSize: 123456 + }; + var logger = new SplunkLogger(config); + + var posts = 0; + + // Wrap _post so we can verify how many times we called it + var _post = logger._post; + logger._post = function(context) { + _post(context, function(err, resp, body) { + posts++; + + assert.ok(!logger._timerID); + assert.strictEqual(posts, 1); + assert.strictEqual(logger.serializedContextQueue.length, 0); + assert.strictEqual(logger.eventsBatchSize, 0); + + assert.ok(!err); + assert.strictEqual(body.code, successBody.code); + assert.strictEqual(body.text, successBody.text); + + done(); + }); + }; + + var payload = { + message: "one event" + }; + logger.send(payload); + }); + it("should not flush events with maxBatchCount=0 (meaning ignore) and large maxBatchSize", function(done) { + var config = { + token: configurationFile.token, + maxBatchCount: 0, + maxBatchSize: 123456 + }; + var logger = new SplunkLogger(config); + + var posts = 0; + + // Wrap _post so we can verify how many times we called it + var _post = logger._post; + logger._post = function(context) { + _post(context, function(err, resp, body) { + posts++; + + assert.ok(!err); + assert.strictEqual(body.code, successBody.code); + assert.strictEqual(body.text, successBody.text); + }); + }; + + var payload = { + message: "one event" + }; + logger.send(payload); + + setTimeout(function() { + assert.ok(!logger._timerID); + assert.strictEqual(posts, 0); + assert.strictEqual(logger.serializedContextQueue.length, 1); + assert.ok(logger.eventsBatchSize > 50); + + done(); + }, 1000); + }); + it("should flush first 2 events after maxBatchCount=2, ignoring large maxBatchSize", function(done) { + var config = { + token: configurationFile.token, + maxBatchCount: 2, + maxBatchSize: 123456 + }; + var logger = new SplunkLogger(config); + + var posts = 0; + + // Wrap _post so we can verify how many times we called it + var _post = logger._post; + logger._post = function(context) { + _post(context, function(err, resp, body) { + posts++; + + assert.ok(!logger._timerID); + assert.strictEqual(posts, 1); + assert.strictEqual(logger.serializedContextQueue.length, 0); + assert.strictEqual(logger.eventsBatchSize, 0); + + assert.ok(!err); + assert.strictEqual(body.code, successBody.code); + assert.strictEqual(body.text, successBody.text); + + done(); + }); + }; + + var payload = { + message: "one event" + }; + logger.send(payload); + + setTimeout(function() { + assert.ok(!logger._timerID); + assert.strictEqual(posts, 0); + assert.strictEqual(logger.serializedContextQueue.length, 1); + assert.ok(logger.eventsBatchSize > 50); + + logger.send(payload); + }, 300); + + setTimeout(function() { + assert.ok(!logger._timerID); + assert.strictEqual(posts, 1); + assert.strictEqual(logger.serializedContextQueue.length, 0); + assert.strictEqual(logger.eventsBatchSize, 0); + }, 400); + }); + it("should flush first event after 200ms, with maxBatchCount=10", function(done) { + var config = { + token: configurationFile.token, + maxBatchCount: 10, + batchInterval: 200 + }; + var logger = new SplunkLogger(config); + + var posts = 0; + + // Wrap _post so we can verify how many times we called it + var _post = logger._post; + logger._post = function(context) { + _post(context, function(err, resp, body) { + posts++; + + assert.ok(!err); + assert.strictEqual(body.code, successBody.code); + assert.strictEqual(body.text, successBody.text); + }); + }; + + var payload = { + message: "one event" + }; + logger.send(payload); + + // Make sure the event wasn't flushed yet + setTimeout(function() { + assert.strictEqual(logger.serializedContextQueue.length, 1); + assert.ok(logger.eventsBatchSize > 50); + }, 150); + + setTimeout(function() { + assert.ok(logger._timerID); + assert.strictEqual(logger._timerDuration, 200); + logger._disableTimer(); + + assert.strictEqual(posts, 1); + assert.strictEqual(logger.serializedContextQueue.length, 0); + assert.strictEqual(logger.eventsBatchSize, 0); + done(); + }, 300); + }); + }); + describe("using custom eventFormatter", function() { + it("should use custom event formatter, instead of the default", function(done) { + var config = { + token: configurationFile.token, + maxBatchCount: 1 + }; + var logger = new SplunkLogger(config); + + logger.eventFormatter = function(message, severity) { + var ret = "[" + severity + "]"; + for (var key in message) { + if (message.hasOwnProperty(key)) { + ret += key + "=" + message[key] + ", "; + } + } + return ret; + }; + + var post = logger._post; + logger._post = function(opts, callback) { + var expected = "[info]some=data, asObject=true, num=123, "; + + assert.ok(opts); + assert.ok(opts.hasOwnProperty("body")); + var body = JSON.parse(opts.body); + assert.ok(body.hasOwnProperty("event")); + assert.ok(body.hasOwnProperty("time")); + + assert.strictEqual(body.event, expected); + + post(opts, callback); + }; + + logger.send({message: {some: "data", asObject: true, num: 123}}, function(err, resp, body) { + assert.ok(!err); + assert.strictEqual(body.code, successBody.code); + assert.strictEqual(body.text, successBody.text); + done(); + }); }); }); }); diff --git a/test/test_utils.js b/test/test_utils.js index 5bde605..3f2126b 100644 --- a/test/test_utils.js +++ b/test/test_utils.js @@ -44,7 +44,6 @@ describe("Utils", function() { var otherFound = utils.formatTime(other); assert.strictEqual(otherFound, otherExpected); }); - // TODO: change these to be strictequal? it("should correctly handle Strings", function() { // Test time in seconds var stringTime = "1372187084"; @@ -161,9 +160,8 @@ describe("Utils", function() { testToArray(1, 2, 3, 4, 5); }); }); - // TODO: rename these tests to the "should..." format describe("chain", function () { - it("single success", function(done) { + it("should succeed with 3 callbacks, passing a single argument through the chain", function(done) { utils.chain([ function(callback) { callback(null, 1); @@ -181,7 +179,7 @@ describe("Utils", function() { } ); }); - it("flat single success", function(done) { + it("should succeed with flat callbacks, passing a single argument through the chain", function(done) { utils.chain( function(callback) { callback(null, 1); @@ -199,7 +197,7 @@ describe("Utils", function() { } ); }); - it("flat multiple success", function(done) { + it("should succeed with flat callbacks, passing multiple arguments through the chain", function(done) { utils.chain( function(callback) { callback(null, 1, 2); @@ -218,7 +216,7 @@ describe("Utils", function() { } ); }); - it("flat add args success", function(done) { + it("should succeed with flat callbacks, appending an argument in the middle of the chain", function(done) { utils.chain( function(callback) { callback(null, 1, 2); @@ -237,7 +235,7 @@ describe("Utils", function() { } ); }); - it("error", function(done) { + it("should surface error from middle of the chain", function(done) { utils.chain([ function(callback) { callback(null, 1, 2); @@ -257,7 +255,7 @@ describe("Utils", function() { } ); }); - it("no tasks", function(done) { + it("should be noop without task callbacks", function(done) { utils.chain([], function(err, val1, val2) { assert.ok(!err); @@ -267,10 +265,10 @@ describe("Utils", function() { } ); }); - it("no args", function() { + it("should be noop without args", function() { utils.chain(); }); - it("no final callback", function(done) { + it("should surface error from first callback in the chain", function(done) { utils.chain([ function(callback) { callback("err"); @@ -282,4 +280,350 @@ describe("Utils", function() { ); }); }); + describe("whilst", function() { + it("should succeed with short counting loop", function(done) { + var i = 0; + utils.whilst( + function() { return i++ < 3; }, + function(callback) { + setTimeout(function() { callback(); }, 0); + }, + function(err) { + assert.ok(!err); + assert.strictEqual(i, 4); + done(); + } + ); + }); + it("should succeed with long counting loop", function(done) { + var i = 0; + utils.whilst( + function() { return i++ < 1000; }, + function(callback) { + setTimeout(function() { callback(); }, 0); + }, + function(err) { + assert.ok(!err); + assert.strictEqual(i, 1001); + done(); + } + ); + }); + it("should pass error to callback function", function(done) { + var i = 0; + utils.whilst( + function() { return i++ < 1000; }, + function(callback) { + setTimeout(function() { callback(i === 1000 ? 1 : null); }, 0); + }, + function(err) { + assert.ok(err); + assert.strictEqual(err, 1); + // Don't execute condition function 1 extra time like above + assert.strictEqual(i, 1000); + done(); + } + ); + }); + it("should never enter loop body when condition loop returns false (default)", function(done) { + var i = false; + utils.whilst( + undefined, + function(callback) { i = true; callback(); }, + function(err) { + assert.ok(!err); + assert.strictEqual(i, false); + done(); + } + ); + }); + it("should be noop with noop loop body", function(done) { + var i = true; + utils.whilst( + function() { + if (i) { + i = false; + return true; + } + else { + return i; + } + }, + undefined, + function (err) { + assert.ok(!err); + done(); + } + ); + }); + it("should succeed with short counting loop, without done callback", function(done) { + var i = 0; + utils.whilst( + function() { return i++ < 3; }, + function(callback) { + setTimeout(function() { callback(); }, 0); + } + ); + + setTimeout(function(){ + if (i !== 4) { + assert.ok(false, "test timed out"); + } + done(); + }, 10); + }); + }); + describe("expBackoff", function() { + it("should error with bad param", function(done) { + utils.expBackoff(null, function(err, timeout) { + assert.ok(err); + assert.strictEqual(err.message, "Must send opts as an object."); + assert.ok(!timeout); + done(); + }); + }); + it("should error with missing opts.attempt", function(done) { + utils.expBackoff({foo: 123}, function(err, timeout) { + assert.ok(err); + assert.strictEqual(err.message, "Must set opts.attempt."); + assert.ok(!timeout); + done(); + }); + }); + it("should have backoff in [20, 40]ms, attempt = 1", function(done) { + utils.expBackoff({attempt: 1}, function(err, timeout) { + assert.ok(!err); + assert.ok(20 <= timeout && timeout <= 40); + done(); + }); + }); + it("should have backoff in [40, 80]ms, attempt = 2", function(done) { + utils.expBackoff({attempt: 2}, function(err, timeout) { + assert.ok(!err); + assert.ok(40 <= timeout && timeout <= 80); + done(); + }); + }); + it("should have backoff in [80, 160]ms, attempt = 3", function(done) { + utils.expBackoff({attempt: 3}, function(err, timeout) { + assert.ok(!err); + assert.ok(80 <= timeout && timeout <= 160); + done(); + }); + }); + it("should have backoff in [160, 320]ms, attempt = 4", function(done) { + utils.expBackoff({attempt: 4}, function(err, timeout) { + assert.ok(!err); + assert.ok(160 <= timeout && timeout <= 320); + done(); + }); + }); + it("should have backoff in [320, 640]ms, attempt = 5", function(done) { + utils.expBackoff({attempt: 5}, function(err, timeout) { + assert.ok(!err); + assert.ok(320 <= timeout && timeout <= 640); + done(); + }); + }); + it("should have backoff of 40ms, attempt = 2, rand = 0", function(done) { + utils.expBackoff({attempt: 2, rand: 0}, function(err, timeout) { + assert.ok(!err); + assert.strictEqual(40, timeout); + done(); + }); + }); + it("should have backoff of 80ms, attempt = 2, rand = 1", function(done) { + utils.expBackoff({attempt: 2, rand: 1}, function(err, timeout) { + assert.ok(!err); + assert.strictEqual(80, timeout); + done(); + }); + }); + it("should have backoff of 80ms, attempt = 2, rand = 1 - no done callback", function(done) { + utils.expBackoff({attempt: 2, rand: 1}); + setTimeout(done, 80); + }); + // TODO: this test is takes 2 minutes, rest of the tests take 12s combined... + // it("should have maximum backoff of 2m (slow running test)", function(done) { + // this.timeout(1000 * 60 * 2 + 500); + // utils.expBackoff({attempt: 100, rand: 0}, function(err, timeout) { + // assert.strictEqual(120000, timeout); + // done(); + // }); + // }); + }); + describe("bind", function() { + it("should successfully bind a function", function(done) { + var f; + (function() { + f = function(a) { + this.a = a; + }; + })(); + var q = {}; + var g = utils.bind(q, f); + g(12); + assert.strictEqual(q.a, 12); + done(); + }); + }); + describe("copyObject", function() { + it("should copy a 5 property object", function() { + var o = { + a: 1, + b: 2, + c: 3, + d: 4, + e: 5, + }; + + var copy = o; + + // Pointing to the same memory block + assert.strictEqual(copy, o); + + // Verify it was a real copy + copy = utils.copyObject(o); + assert.notStrictEqual(copy, o); + assert.strictEqual(Object.keys(copy).length, 5); + assert.strictEqual(copy.a, o.a); + assert.strictEqual(copy.b, o.b); + assert.strictEqual(copy.c, o.c); + assert.strictEqual(copy.d, o.d); + assert.strictEqual(copy.e, o.e); + + // Verify changing original object vals doesn't change copy's vals + for (var k in o) { + if (o.hasOwnProperty(k)){ + o[k]++; + } + } + assert.notStrictEqual(copy.a, o.a); + assert.notStrictEqual(copy.b, o.b); + assert.notStrictEqual(copy.c, o.c); + assert.notStrictEqual(copy.d, o.d); + assert.notStrictEqual(copy.e, o.e); + + }); + }); + describe("copyArray", function() { + it("should copy a 5 element array", function() { + var a = [0, 1, 2, 3, 4]; + + var copy = a; + + // Pointing to the same memory block + assert.strictEqual(copy, a); + + // Verify it was a real copy + copy = utils.copyArray(a); + assert.notStrictEqual(copy, a); + assert.strictEqual(Object.keys(copy).length, 5); + assert.strictEqual(copy[0], a[0]); + assert.strictEqual(copy[1], a[1]); + assert.strictEqual(copy[2], a[2]); + assert.strictEqual(copy[3], a[3]); + assert.strictEqual(copy[4], a[4]); + + // Verify changing original array vals doesn't change copy's vals + for (var k in a) { + a[k]++; + } + assert.notStrictEqual(copy[0], a[0]); + assert.notStrictEqual(copy[1], a[1]); + assert.notStrictEqual(copy[2], a[2]); + assert.notStrictEqual(copy[3], a[3]); + assert.notStrictEqual(copy[4], a[4]); + + }); + }); + describe("orByProp", function() { + it("should pick first value of 2", function() { + var a = { + x: "x value" + }; + var b = { + y: "y value" + }; + + assert.strictEqual(utils.orByProp("x", a, b), "x value"); + assert.strictEqual(utils.orByProp("y", b, a), "y value"); + }); + it("should pick second value of 2, when first is undefined", function() { + var a = { + x: "x value" + }; + var b = { + y: "y value" + }; + + assert.strictEqual(utils.orByProp("x", b, a), "x value"); + assert.strictEqual(utils.orByProp("y", a, b), "y value"); + }); + it("should ignore a null argument", function() { + var a = { + x: "x value" + }; + var b = { + y: "y value" + }; + + assert.strictEqual(utils.orByProp("x", null, b, a), "x value"); + assert.strictEqual(utils.orByProp("y", null, a, b), "y value"); + }); + }); + describe("orByFalseyProp", function() { + it("should pick first value of 2", function() { + var a = { + x: false + }; + var b = { + y: true + }; + + assert.strictEqual(utils.orByFalseyProp("x", a, b), false); + assert.strictEqual(utils.orByFalseyProp("y", b, a), true); + }); + it("should pick second value of 2", function() { + var a = { + x: false + }; + var b = { + y: true + }; + + assert.strictEqual(utils.orByFalseyProp("x", b, a), false); + assert.strictEqual(utils.orByFalseyProp("y", a, b), true); + }); + }); + describe("validateNonNegativeInt", function() { + it("should error when value is NaN", function() { + try { + utils.validateNonNegativeInt(null, "test"); + assert.ok(false, "Expected an error."); + } + catch (err) { + assert.ok(err); + assert.strictEqual(err.message, "test must be a number, found: NaN"); + } + }); + it("should error when value is negative", function() { + try { + utils.validateNonNegativeInt(-1, "test"); + assert.ok(false, "Expected an error."); + } + catch (err) { + assert.ok(err); + assert.strictEqual(err.message, "test must be a positive number, found: -1"); + } + }); + it("should return the value when it's 0", function() { + var valid = utils.validateNonNegativeInt(0, "test"); + assert.strictEqual(valid, 0); + }); + it("should return the value when it's positive", function() { + var valid = utils.validateNonNegativeInt(5, "test"); + assert.strictEqual(valid, 5); + }); + }); }); \ No newline at end of file diff --git a/utils.js b/utils.js index eeb32b8..83f82a4 100644 --- a/utils.js +++ b/utils.js @@ -128,4 +128,178 @@ utils.chain = function(tasks, callback) { } }; +/** + * Asynchronous while loop. + * + * @param {function} [condition] - A function returning a boolean, the loop condition. + * @param {function} [body] - A function, the loop body. + * @param {function} [callback] - Final callback. + * @static + */ +utils.whilst = function (condition, body, callback) { + condition = condition || function() { return false; }; + body = body || function(done){ done(); }; + callback = callback || function() {}; + + var wrappedCallback = function(err) { + if (err) { + callback(err); + } + else { + utils.whilst(condition, body, callback); + } + }; + + if (condition()) { + body(wrappedCallback); + } + else { + callback(null); + } +}; + +/** + * Waits using exponential backoff. + * + * @param {object} [opts] - Settings for this function. Expected keys: attempt, rand. + * @param {function} [callback] - A callback function: function(err, timeout). + */ +utils.expBackoff = function(opts, callback) { + callback = callback || function(){}; + if (!opts || typeof opts !== "object") { + callback(new Error("Must send opts as an object.")); + } + else if (opts && !opts.hasOwnProperty("attempt")) { + callback(new Error("Must set opts.attempt.")); + } + else { + + var min = 10; + var max = 1000 * 60 * 2; // 2 minutes is a reasonable max delay + + var rand = Math.random(); + if (opts.hasOwnProperty("rand")) { + rand = opts.rand; + } + rand++; + + var timeout = Math.round(rand * min * Math.pow(2, opts.attempt)); + + timeout = Math.min(timeout, max); + setTimeout( + function() { + callback(null, timeout); + }, + timeout + ); + } +}; + +/** + * Binds a function to an instance of an object. + * + * @param {object} [self] - An object to bind the fn function parameter to. + * @param {object} [fn] - A function to bind to the self argument. + * @returns {function} + * @static + */ +utils.bind = function(self, fn) { + return function () { + return fn.apply(self, arguments); + }; +}; + +/** + * Copies all properties into a new object which is returned. + * + * @param {object} [obj] - Object to copy properties from. + */ +utils.copyObject = function(obj) { + var ret = {}; + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + ret[key] = obj[key]; + } + } + return ret; +}; + +/** + * Copies all elements into a new array which is returned. + * + * @param {array} [arr] - Array to copy elements from. + * @returns {array} + * @static + */ +utils.copyArray = function(arr) { + var ret = []; + for (var i = 0; arr && i < arr.length; i++) { + ret[i] = arr[i]; + } + return ret; +}; + +/** + * Takes a property name, then any number of objects as arguments + * and performs logical OR operations on them one at a time + * Returns true as soon as a truthy + * value is found, else returning false. + * + * @param {string} [prop] - property name for other arguments. + * @returns {boolean} + * @static + */ +utils.orByProp = function(prop) { + var ret = false; + for (var i = 1; !ret && i < arguments.length; i++) { + if (arguments[i]) { + ret = ret || arguments[i][prop]; + } + } + return ret; +}; + +/** + * Like utils.orByProp() but for a falsey property. + * The first argument after prop with that property + * defined will be returned. + * Useful for booleans and numbers. + * + * @param {string} [prop] - property name for other arguments. + * @returns {boolean} + * @static + */ +utils.orByFalseyProp = function(prop) { + var ret = null; + // Logic is reversed here, first value wins + for (var i = arguments.length - 1; i > 0; i--) { + if (arguments[i] && arguments[i].hasOwnProperty(prop)) { + ret = arguments[i][prop]; + } + } + return ret; +}; + + /** + * Tries to validate the value parameter as a non-negative + * integer. + * + * @param {number} [value] - Some value, expected to be a positive integer. + * @param {number} [label] - Human readable name for value + * for error messages. + * @returns {number} + * @throws Will throw an error if the value parameter cannot by parsed as an integer. + * @static + */ +utils.validateNonNegativeInt = function(value, label) { + value = parseInt(value, 10); + if (isNaN(value)) { + throw new Error(label + " must be a number, found: " + value); + } + else if (value < 0) { + throw new Error(label + " must be a positive number, found: " + value); + } + return value; +}; + module.exports = utils; \ No newline at end of file