static getStats (req, res) {
    const query = {
      from: 0,
      size: 10000,
      query: {
        bool: {
          filter: [],
          should: [],
          must_not: [
            { term: { hide: true } }
          ]
        }
      }
    };

    if (req.query.filter !== undefined && req.query.filter !== '') {
      const names = req.query.filter.split(',');
      for (let n of names) {
        n = n.trim();
        if (n !== '') {
          if (n.endsWith('$')) { n = n.slice(0, -1); } else { n += '.*'; }
          query.query.bool.should.push({
            regexp: { nodeName: n }
          });
        }
      }
    }
    ViewerUtils.addCluster(req.query.cluster, query);

    const rquery = {
      query: { term: { locked: 0 } },
      size: 0,
      aggregations: {
        buckets: {
          terms: { field: 'node', size: 1000 },
          aggregations: {
            first: { min: { field: 'first' } }
          }
        }
      }
    };
    ViewerUtils.addCluster(req.query.cluster, rquery);

    if (req.query.hide !== undefined && req.query.hide !== 'none') {
      if (req.query.hide === 'old' || req.query.hide === 'both') {
        query.query.bool.filter.push({ range: { currentTime: { gte: 'now-5m' } } });
      }
      if (req.query.hide === 'nosession' || req.query.hide === 'both') {
        query.query.bool.filter.push({ range: { monitoring: { gte: '1' } } });
      }
    }

    const now = Math.floor(Date.now() / 1000);

    Promise.all([Db.search('stats', 'stat', query),
      Db.numberOfDocuments('stats', rquery.cluster ? { cluster: rquery.cluster } : {}),
      Db.search('files', 'file', rquery)
    ]).then(([stats, total, retention]) => {
      if (stats.error) { throw stats.error; }

      if (retention.aggregations.buckets && retention.aggregations.buckets.buckets) {
        retention = ViewerUtils.arrayToObject(retention.aggregations.buckets.buckets, 'key');
      } else {
        retention = {};
      }

      const results = { total: stats.hits.total, results: [] };

      for (let i = 0, ilen = stats.hits.hits.length; i < ilen; i++) {
        const fields = stats.hits.hits[i]._source || stats.hits.hits[i].fields;
        if (stats.hits.hits[i]._source) {
          ViewerUtils.mergeUnarray(fields, stats.hits.hits[i].fields);
        }
        fields.id = stats.hits.hits[i]._id;

        if (retention[fields.id]) {
          fields.retention = now - retention[fields.id].first.value;
        } else {
          fields.retention = 0;
        }

        fields.deltaBytesPerSec = Math.floor(fields.deltaBytes * 1000.0 / fields.deltaMS);
        fields.deltaWrittenBytesPerSec = Math.floor(fields.deltaWrittenBytes * 1000.0 / fields.deltaMS);
        fields.deltaUnwrittenBytesPerSec = Math.floor(fields.deltaUnwrittenBytes * 1000.0 / fields.deltaMS);
        fields.deltaBitsPerSec = Math.floor(fields.deltaBytes * 1000.0 / fields.deltaMS * 8);
        fields.deltaPacketsPerSec = Math.floor(fields.deltaPackets * 1000.0 / fields.deltaMS);
        fields.deltaSessionsPerSec = Math.floor(fields.deltaSessions * 1000.0 / fields.deltaMS);
        fields.deltaSessionBytesPerSec = Math.floor(fields.deltaSessionBytes * 1000.0 / fields.deltaMS);
        fields.sessionSizePerSec = Math.floor(fields.deltaSessionBytes / fields.deltaSessions);
        fields.deltaDroppedPerSec = Math.floor(fields.deltaDropped * 1000.0 / fields.deltaMS);
        fields.deltaFragsDroppedPerSec = Math.floor(fields.deltaFragsDropped * 1000.0 / fields.deltaMS);
        fields.deltaOverloadDroppedPerSec = Math.floor(fields.deltaOverloadDropped * 1000.0 / fields.deltaMS);
        fields.deltaESDroppedPerSec = Math.floor(fields.deltaESDropped * 1000.0 / fields.deltaMS);
        fields.deltaDupDroppedPerSec = Math.floor(fields.deltaDupDropped * 1000.0 / fields.deltaMS) || 0;
        fields.deltaTotalDroppedPerSec = Math.floor((fields.deltaDropped + fields.deltaOverloadDropped) * 1000.0 / fields.deltaMS);
        fields.runningTime = fields.currentTime - fields.startTime;
        results.results.push(fields);
      }

      // sort after all the results are aggregated
      req.query.sortField = req.query.sortField || 'nodeName';
      if (results.results[0] && results.results[0][req.query.sortField] !== undefined) { // make sure the field exists to sort on
        results.results = results.results.sort((a, b) => {
          if (req.query.desc === 'true') {
            if (!isNaN(a[req.query.sortField])) {
              return b[req.query.sortField] - a[req.query.sortField];
            } else {
              return b[req.query.sortField].localeCompare(a[req.query.sortField]);
            }
          } else {
            if (!isNaN(a[req.query.sortField])) {
              return a[req.query.sortField] - b[req.query.sortField];
            } else {
              return a[req.query.sortField].localeCompare(b[req.query.sortField]);
            }
          }
        });
      }

      const from = +req.query.start || 0;
      const stopLen = from + (+req.query.length || 500);

      const r = {
        recordsTotal: total.count,
        recordsFiltered: results.results.length,
        data: results.results.slice(from, stopLen)
      };

      res.send(r);
    }).catch((err) => {
      console.log(`ERROR - ${req.method} /api/stats`, query, util.inspect(err, false, 50));
      res.send({ recordsTotal: 0, recordsFiltered: 0, data: [] });
    });
  }
GET - /api/stats

Fetches a list of stats for each node in the cluster.
@name /stats
@param {string} filter - Search text to filter the list of nodes by.
@param {number} length=500 - The number of nodes to return. Defaults to 500.
@param {number} start=0 - The entry to start at. Defaults to 0.
@param {string} sortField=nodeName - The field to sort the node list by.
@param {string} desc=false - Whether to return the results in descending order. Defaults to "false".
@param {string} hide - Which nodes to exclude from the results. Options include:
     none - show all nodes.
     old - hide out of date nodes (nodes whose current time is behind by at least 5 minutes).
     nosession - hide nodes without sessions.
     both - hide out of date nodes and nodes without sessions.
@returns {array} data - List of nodes with their corresponding stats.
@returns {number} recordsTotal - The total number of nodes.
@returns {number} recordsFiltered - The number of nodes returned in this result.