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