var util = require('util'), _ = require('lodash'), Stats = require('fast-stats').Stats; var defaults = { timesLength: 60, countsLength: 120, healthLength: 10 }; /** * Circuit breaker statistics * * @constructor * @param {object} options * @param {integer} options.timesLength - Amount of intervals to keep for call times statistics * (default: 60) * @param {integer} options.countsLength - Amount of intervals to keep for summarized counters * (default: 120) * @param {integer} options.healthLength - Amount of intervals to keep for the health summary * (default: 10) */ function CBStats(options) { options = _.extend({}, options || {}); var opts = _.defaults(options, defaults); var timeStats = new Stats(), intervalLengths = [], counters = [], countersSum = makeStats(), healths = [], healthsSum = makeStats(), summary = {}; delete countersSum.active; delete countersSum.queued; delete healthsSum.active; delete healthsSum.queued; function addInterval(interval) { addTimeStats(interval); addCounters(interval); updateHealthWindow(interval); } function addTimeStats(interval) { intervalLengths.push(interval.times.length); timeStats.push(interval.times); var extra = intervalLengths.length - opts.timesLength; for (var i = 0; i < extra; i++) { var n = intervalLengths.shift(); for (var j = 0; j < n; j++) { timeStats.shift(); } } /** * @typedef {object} CBStats~TimesSummary * @property {float} amean - Arithmetic mean * @property {float} median - Median * @property {integer} p900 - 90th percentile * @property {integer} p990 - 99th percentile */ summary.times = { amean: timeStats.amean(), median: timeStats.median(), p900: timeStats.percentile(90), p990: timeStats.percentile(99) }; } function addCounters(interval) { var stats = getStatsFromInterval(interval); counters.push(stats); if (counters.length > opts.countsLength) { var out = counters.shift(); substractStats(out, countersSum); } addStats(stats, countersSum); /** * @typedef {object} CBStats~CountsSummary * @property {integer} total * @property {integer} success * @property {integer} totalErrors * @property {object} errors - Error counts by name */ summary.counts = countersSum; } function getStatsFromInterval(interval) { var stats = makeStats(); stats.total = interval.total; stats.success = interval.success; stats.totalErrors = interval.totalErrors; stats.active = interval.active; stats.queued = interval.queued; for (var err in interval.errors) { stats.errors[err] = interval.errors[err]; } stats.start = interval.start; stats.end = interval.end; return stats; } function updateHealthWindow(interval) { var stats = getStatsFromInterval(interval); healths.push(stats); if (healths.length > opts.healthLength) { var out = healths.shift(); substractStats(out, healthsSum); } addStats(stats, healthsSum); var wlength = (interval.end - healths[0].start) / 1000; /** * @typedef CBStats~HealthSummary * @property {integer} total - Total amount of calls * @property {integer} success - Total amount of successful calls * @property {integer} totalErrors - Total errors * @property {integer} timeouts - Total amount of TimeoutError errors * @property {integer} rejected - Total amount of OpenCircuitError errors * @property {integer} otherErrors - Total amount of errors which are neither timeouts nor rejections * @property {object} errors - Total error counts by name * @property {float} callRate - Calls per second * @property {integer} active - Current active calls * @property {integer} queued - Current queued calls */ summary.health = { total: healthsSum.total, success: healthsSum.success, totalErrors: healthsSum.totalErrors, timeouts: healthsSum.errors.TimeoutError || 0, rejected: healthsSum.errors.OpenCircuitError || 0, otherErrors: healthsSum.totalErrors - (healthsSum.errors.TimeoutError || 0) - (healthsSum.errors.OpenCircuitError || 0), errors: healthsSum.errors, callRate: wlength ? healthsSum.total / wlength : 0, errorRate: healthsSum.total ? healthsSum.totalErrors / healthsSum.total : 0, active: interval.active, queued: interval.queued, state: interval.state }; } function substractStats(stats, cummulative) { cummulative.total -= stats.total; cummulative.success -= stats.success; cummulative.totalErrors -= stats.totalErrors; for (var err in stats.errors) { cummulative.errors[err] -= stats.errors[err]; if (cummulative.errors[err] === 0) { delete cummulative.errors[err]; } } } function addStats(stats, cummulative) { cummulative.total += stats.total; cummulative.success += stats.success; cummulative.totalErrors += stats.totalErrors; if (!cummulative.errors) { cummulative.errors = {}; } for (var err in stats.errors) { if (!cummulative.errors[err]) { cummulative.errors[err] = 0; } cummulative.errors[err] += stats.errors[err]; } } function makeStats() { return { total: 0, success: 0, totalErrors: 0, active: 0, queued: 0, errors: {} }; } /** * Add intervals emitted by the {@link CircuitBreaker#event:interval} event. * * @param {object[]} intervals - Array of {@link CircuitBreaker#event:interval} instances */ this.add = function(intervals) { for (var i = 0, l = intervals.length; i < l; i++) { addInterval(intervals[i]); } }; /** * Get statistics summary * * @return {CBStats~Summary} stats * */ this.getSummary = function() { return summary; }; this.getCounters = function() { return counters; }; } /** * @typedef {object} CBStats~Summary * @property {CBStats~TimesSummary} times - Times summary * @property {CBStats~CountsSummary} counts - Cummulative counts summary * @property {CBStats~HealthSummary} health - Health summary */ module.exports = CBStats;