Source: stats.js

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;