static getESStats (req, res) {
    let stats = [];

    Promise.all([
      Db.nodesStatsCache(req.query.cluster),
      Db.nodesInfoCache(req.query.cluster),
      Db.masterCache(req.query.cluster),
      Db.allocation(req.query.cluster),
      Db.getClusterSettingsCache({ flat_settings: true, cluster: req.query.cluster })
    ]).then(([nodesStats, nodesInfo, master, { body: allocation }, { body: settings }]) => {
      const shards = new Map(allocation.map(i => [i.node, parseInt(i.shards, 10)]));

      let ipExcludes = [];
      if (settings.persistent['cluster.routing.allocation.exclude._ip']) {
        ipExcludes = settings.persistent['cluster.routing.allocation.exclude._ip'].split(',');
      }

      let nodeExcludes = [];
      if (settings.persistent['cluster.routing.allocation.exclude._name']) {
        nodeExcludes = settings.persistent['cluster.routing.allocation.exclude._name'].split(',');
      }

      const now = new Date().getTime();
      while (internals.previousNodesStats.length > 1 && internals.previousNodesStats[1].timestamp + 10000 < now) {
        internals.previousNodesStats.shift();
      }

      let regex;
      if (req.query.filter !== undefined) {
        try {
          regex = new RE2(req.query.filter);
        } catch (e) {
          return res.serverError(500, `Regex Error: ${e}`);
        }
      }

      const nodeKeys = Object.keys(nodesStats.nodes);
      for (let n = 0, nlen = nodeKeys.length; n < nlen; n++) {
        const node = nodesStats.nodes[nodeKeys[n]];

        if (nodeKeys[n] === 'timestamp' || (regex && !node.name.match(regex))) { continue; }

        let read = 0;
        let write = 0;
        let rejected = 0;
        let completed = 0;

        const writeInfo = node.thread_pool.bulk || node.thread_pool.write;

        const oldnode = internals.previousNodesStats[0][nodeKeys[n]];
        if (oldnode !== undefined && node.fs.io_stats !== undefined && oldnode.fs.io_stats !== undefined && 'total' in node.fs.io_stats) {
          const timediffsec = (node.timestamp - oldnode.timestamp) / 1000.0;
          read = Math.max(0, Math.ceil((node.fs.io_stats.total.read_kilobytes - oldnode.fs.io_stats.total.read_kilobytes) / timediffsec * 1024));
          write = Math.max(0, Math.ceil((node.fs.io_stats.total.write_kilobytes - oldnode.fs.io_stats.total.write_kilobytes) / timediffsec * 1024));

          const writeInfoOld = oldnode.thread_pool.bulk || oldnode.thread_pool.write;

          completed = Math.max(0, Math.ceil((writeInfo.completed - writeInfoOld.completed) / timediffsec));
          rejected = Math.max(0, Math.ceil((writeInfo.rejected - writeInfoOld.rejected) / timediffsec));
        }

        const ip = (node.ip ? node.ip.split(':')[0] : node.host);

        let threadpoolInfo;
        let version = '';
        let molochtype;
        let molochzone;
        if (nodesInfo.nodes[nodeKeys[n]]) {
          threadpoolInfo = nodesInfo.nodes[nodeKeys[n]].thread_pool.bulk || nodesInfo.nodes[nodeKeys[n]].thread_pool.write;
          version = nodesInfo.nodes[nodeKeys[n]].version;
          if (nodesInfo.nodes[nodeKeys[n]].attributes) {
            molochtype = nodesInfo.nodes[nodeKeys[n]].attributes.molochtype;
            molochzone = nodesInfo.nodes[nodeKeys[n]].attributes.molochzone;
          }
        } else {
          threadpoolInfo = { queue_size: 0 };
        }

        stats.push({
          name: node.name,
          ip,
          ipExcluded: ipExcludes.includes(ip),
          nodeExcluded: nodeExcludes.includes(node.name),
          storeSize: node.indices.store.size_in_bytes,
          freeSize: node.roles.some(str => str.startsWith('data')) ? node.fs.total.available_in_bytes : 0,
          docs: node.indices.docs.count,
          searches: node.indices.search.query_current,
          searchesTime: node.indices.search.query_time_in_millis,
          heapSize: node.jvm.mem.heap_used_in_bytes,
          nonHeapSize: node.jvm.mem.non_heap_used_in_bytes,
          uptime: Math.floor(node.jvm.uptime_in_millis / (1000 * 60)),
          cpu: node.process.cpu.percent,
          read,
          write,
          writesRejected: writeInfo.rejected,
          writesCompleted: writeInfo.completed,
          writesRejectedDelta: rejected,
          writesCompletedDelta: completed,
          writesQueueSize: threadpoolInfo.queue_size,
          load: node.os.cpu.load_average['5m'] ?? node.os.cpu.load_average['1m'],
          version,
          molochtype,
          molochzone,
          roles: node.roles,
          isMaster: (master.length > 0 && node.name === master[0].node),
          shards: shards.get(node.name) || 0,
          segments: node.indices.segments.count || 0
        });
      }

      if (req.query.sortField && stats.length > 1) {
        const field = req.query.sortField === 'nodeName' ? 'name' : req.query.sortField;
        if (typeof (stats[0][field]) === 'string') {
          if (req.query.desc === 'true') {
            stats = stats.sort((a, b) => { return b[field].localeCompare(a[field]); });
          } else {
            stats = stats.sort((a, b) => { return a[field].localeCompare(b[field]); });
          }
        } else {
          if (req.query.desc === 'true') {
            stats = stats.sort((a, b) => { return b[field] - a[field]; });
          } else {
            stats = stats.sort((a, b) => { return a[field] - b[field]; });
          }
        }
      }

      nodesStats.nodes.timestamp = new Date().getTime();
      internals.previousNodesStats.push(nodesStats.nodes);

      res.send({
        data: stats,
        recordsFiltered: stats.length,
        recordsTotal: (nodeKeys.includes('timestamp')) ? nodeKeys.length - 1 : nodeKeys.length
      });
    }).catch((err) => {
      console.log(`ERROR - ${req.method} /api/esstats`, util.inspect(err, false, 50));
      return res.send({
        data: [],
        recordsTotal: 0,
        recordsFiltered: 0
      });
    });
  }
GET - /api/esstats

Fetches a list of stats for each OpenSearch/Elasticsearch cluster.
@name /esstats
@param {string} filter - Search text to filter the list of OpenSearch/Elasticsearch clusters by.
@param {string} sortField=nodeName - The field to sort the OpenSearch/Elasticsearch clusters list by.
@param {string} desc=false - Whether to return the results in descending order. Defaults to "false".
@returns {array} data - List of ES clusters with their corresponding stats.
@returns {number} recordsTotal - The total number of ES clusters.
@returns {number} recordsFiltered - The number of ES clusters returned in this result.