rsercano/mongoclient

View on GitHub
client/imports/ui/db_stats/index.js

Summary

Maintainability
D
3 days
Test Coverage
import { SessionManager, ErrorHandler, UIComponents, Notification } from '/client/imports/modules';
import { Communicator, ReactivityProvider } from '/client/imports/facades';
import Helper from '/client/imports/helpers/helper';
import moment from 'moment';
import $ from 'jquery';

const DBStats = function () {
  this.interval = null;
  this.memoryChart = null;
  this.connectionsChart = null;
  this.networkChart = null;
  this.opCountersChart = null;
  this.queuedReadWriteChart = null;
  this.activeReadWriteChart = null;
  this.dataCountToKeep = 15;
  this.previousTopData = {};
  this.lineOptions = {
    series: {
      lines: {
        show: true,
        lineWidth: 3,
        fill: true,
        fillColor: {
          colors: [{
            opacity: 0.0,
          }, {
            opacity: 0.0,
          }],
        },
      },
      points: {
        show: true,
      },
    },
    xaxis: {
      show: true,
      tickFormatter(val) {
        return moment(val).format('HH:mm:ss');
      },
    },
    colors: ['#1ab394', '#ff0f0f'],
    grid: {
      color: '#999999',
      hoverable: true,
      clickable: true,
      tickColor: '#D4D4D4',
      borderWidth: 0,
    },
    legend: {
      position: 'ne',
    },
    tooltip: true,
    tooltipOpts: {
      content: '%y',
    },
    zoom: {
      interactive: true,
    },
    pan: {
      interactive: true,
    },
  };
};

const addData = function (index, existingData, data) {
  if (existingData.length >= (index + 1) && existingData[index].data && data[index].data) existingData[index].data.push(...data[index].data);
};

const replaceData = function (index, existingData, dataCountToKeep) {
  if (existingData.length >= (index + 1) && existingData[index].data) existingData[index].data = existingData[index].data.slice(1, dataCountToKeep);
};

const mergeChartData = function (existingData, data, dataCountToKeep) {
  if (existingData[0].data.length >= dataCountToKeep) {
    existingData[0].data = existingData[0].data.slice(1, dataCountToKeep);

    replaceData(1, existingData, dataCountToKeep);
    replaceData(2, existingData, dataCountToKeep);
  }

  existingData[0].data.push(...data[0].data);
  addData(1, existingData, data);
  addData(2, existingData, data);

  return existingData;
};

const getCorrectScales = function (settings, forMemory) {
  if (forMemory) return Helper.getScaleAndText(settings.scale, true);
  return Helper.getScaleAndText(settings.scale);
};

const initChart = function ({ chartVariable, spanSelector, divSelector, data, total, translateKey = 'total', lineOptions, merge = true }) {
  if (!SessionManager.get(SessionManager.strSessionCollectionNames)) return;

  if (total) spanSelector.html(`, ${Helper.translate({ key: translateKey })}: ${total}`);

  if (!data || data.length === 0) {
    divSelector.html(Helper.translate({ key: 'feature_not_supported_mongodb_version' }));
    return;
  }

  if (divSelector.find('.flot-base').length <= 0) {
    try {
      chartVariable = $.plot(divSelector, data, lineOptions);
    } catch (e) {
      chartVariable = null;
    }
  } else {
    const mergedData = merge ? mergeChartData(chartVariable.getData(), data, this.dataCountToKeep) : data;

    chartVariable.setData(mergedData);
    chartVariable.setupGrid();
    chartVariable.draw();
  }

  return chartVariable;
};

const proceedGettingReadWriteDataFromGlobalLock = function (result, data, lockField) {
  if (result.globalLock && result.globalLock[lockField]) {
    const readers = [];
    const writers = [];
    const time = new Date().getTime();

    readers.push([time, result.globalLock[lockField].readers]);
    writers.push([time, result.globalLock[lockField].writers]);

    data.push({ data: readers, label: Helper.translate({ key: 'readers' }) });
    data.push({ data: writers, label: Helper.translate({ key: 'writers' }) });

    return result.globalLock[lockField].total;
  }
};

DBStats.prototype = {
  clear() {
    if (this.interval) {
      clearInterval(this.interval);
      this.interval = null;
    }
  },

  init() {
    const fetchedSettings = ReactivityProvider.findOne(ReactivityProvider.types.Settings);
    if (fetchedSettings.showDBStats && !this.interval) {
      this.dataCountToKeep = (fetchedSettings.maxLiveChartDataPoints && fetchedSettings.maxLiveChartDataPoints > 0) ? fetchedSettings.maxLiveChartDataPoints : 15;
      this.interval = setInterval(() => {
        this.fetchStats();
        this.fetchStatus();
      }, fetchedSettings.dbStatsScheduler ? fetchedSettings.dbStatsScheduler : 3000);
    }
  },

  subscribe() {
    Notification.start('#btnSubscribe');

    Communicator.call({
      methodName: 'handleSubscriber',
      args: { email: $('#txtEmailToSubscribe').val() },
      callback: (err) => {
        if (err) ErrorHandler.showMeteorFuncError(err);
        else Notification.success('thanks-for-subscription');
      }
    });
  },

  fetchStats() {
    if (SessionManager.get(SessionManager.strSessionCollectionNames)) {
      const settings = ReactivityProvider.findOne(ReactivityProvider.types.Settings);
      Communicator.call({
        methodName: 'dbStats',
        callback: (err, result) => {
          if (err || result.error) {
            ErrorHandler.showMeteorFuncError(err, result);
            SessionManager.set(SessionManager.strSessionDBStats, null);
          } else {
            this.convertInformationsToCorrectUnit(result.result, settings);
            SessionManager.set(SessionManager.strSessionDBStats, result.result);
          }
        }
      });
    }
  },

  poopulateActiveReadWriteData(result, data) {
    return proceedGettingReadWriteDataFromGlobalLock(result, data, 'activeClients');
  },

  fetchStatus() {
    if (SessionManager.get(SessionManager.strSessionCollectionNames)) {
      const settings = ReactivityProvider.findOne(ReactivityProvider.types.Settings);
      if (settings) {
        Communicator.call({
          methodName: 'serverStatus',
          callback: (err, result) => {
            if (err || result.error) {
              const errorMessage = result.error ? result.error.message : err.message;
              $('#errorMessage').text(Helper.translate({ key: 'db_stats_error', options: { error: errorMessage } }));
              SessionManager.set(SessionManager.strSessionServerStatus, null);
            } else {
              SessionManager.set(SessionManager.strSessionServerStatus, result.result);
              const memoryData = []; const connectionsData = []; const networkData = []; const opCountersData = []; const queuedReadWriteData = []; const activeReadWriteData = [];
              const memoryText = this.populateMemoryData(result.result, memoryData, settings);
              const availableConnections = this.populateConnectionData(result.result, connectionsData);
              const totalRequests = this.populateNetworkData(result.result, networkData, settings);
              this.populateOPCountersData(result.result, opCountersData);
              const totalQueuedReadWrite = this.poopulateQueuedReadWriteData(result.result, queuedReadWriteData);
              const totalActiveReadWrite = this.poopulateActiveReadWriteData(result.result, activeReadWriteData);

              this.initMemoryChart(memoryData, memoryText);
              this.initConnectionsChart(connectionsData, availableConnections);
              this.initNetworkChart(networkData, totalRequests);
              this.initOperationCountersChart(opCountersData);
              this.initQueuedReadWriteChart(queuedReadWriteData, totalQueuedReadWrite);
              this.initActiveReadWriteChart(activeReadWriteData, totalActiveReadWrite);
            }
          }
        });

        Communicator.call({
          methodName: 'top',
          callback: (err, result) => {
            if (result && result.result && result.result.totals) {
              const collectionReadWriteData = this.populateTopReadWriteData(result.result.totals);
              this.initCollectionsReadWriteTable(collectionReadWriteData);
              this.previousTopData = result.result.totals;
            }
          }
        });
      }
    }
  },

  initCollectionsReadWriteTable(collectionReadWriteData) {
    UIComponents.DataTable.setupDatatable({
      selectorString: '#tblCollectionsReadWrite',
      columns: [
        { data: 'collection' },
        { data: 'read' },
        { data: 'write' },
      ],
      lengthMenu: [[5, 10, 25, -1], [5, 10, 25, 'All']],
      data: collectionReadWriteData
    });
  },

  poopulateQueuedReadWriteData(result, data) {
    return proceedGettingReadWriteDataFromGlobalLock(result, data, 'currentQueue');
  },

  populateTopReadWriteData(data) {
    const result = [];

    Object.keys(data).forEach((collectionName) => {
      if (collectionName !== 'note') {
        const readTime = data[collectionName].readLock.time;
        const readCount = data[collectionName].readLock.count;
        const writeTime = data[collectionName].writeLock.time;
        const writeCount = data[collectionName].writeLock.count;

        let previousReadTime; let previousReadCount; let previousWriteTime; let previousWriteCount;
        if (this.previousTopData[collectionName]) {
          previousReadTime = this.previousTopData[collectionName].readLock.time;
          previousReadCount = this.previousTopData[collectionName].readLock.count;
          previousWriteTime = this.previousTopData[collectionName].writeLock.time;
          previousWriteCount = this.previousTopData[collectionName].writeLock.count;
        } else {
          previousReadTime = readTime;
          previousReadCount = readCount;
          previousWriteTime = writeTime;
          previousWriteCount = writeCount;
        }

        const calculatedReadTime = Number((readTime - previousReadTime) / (readCount - previousReadCount));
        const calculatedWriteTime = Number((writeTime - previousWriteTime) / (writeCount - previousWriteCount));

        result.push({
          collection: collectionName,
          read: Number.isNaN(calculatedReadTime) ? 0 : calculatedReadTime.toFixed(2),
          write: Number.isNaN(calculatedWriteTime) ? 0 : calculatedWriteTime.toFixed(2),
        });
      }
    });

    return result;
  },

  populateOPCountersData(result, data) {
    if (result.opcounters) {
      const counts = [
        [0, result.opcounters.insert],
        [1, result.opcounters.query],
        [2, result.opcounters.update],
        [3, result.opcounters.delete],
        [4, result.opcounters.getmore],
      ];

      data.push({ label: Helper.translate({ key: 'counts' }), data: counts, color: '#1ab394' });
    }
  },

  populateConnectionData(result, data) {
    if (result.connections) {
      const currentData = []; const totalCreatedData = [];
      const time = new Date().getTime();

      currentData.push([time, Math.round(result.connections.current * 100) / 100]);
      totalCreatedData.push([time, Math.round(result.connections.totalCreated * 100) / 100]);

      data.push({ data: currentData, label: Helper.translate({ key: 'active' }) });
      data.push({ data: totalCreatedData, label: Helper.translate({ key: 'total_created' }) });

      return result.connections.available;
    }
  },

  populateNetworkData(result, data, settings) {
    if (result.network) {
      const bytesInData = []; const bytesOutData = [];
      const time = new Date().getTime();

      const { scale, text } = getCorrectScales(settings);

      bytesInData.push([time, Math.round((result.network.bytesIn / scale) * 100) / 100]);
      bytesOutData.push([time, Math.round((result.network.bytesOut / scale) * 100) / 100]);

      data.push({ data: bytesInData, label: Helper.translate({ key: 'incoming', options: { data: text } }) });
      data.push({ data: bytesOutData, label: Helper.translate({ key: 'outgoing', options: { data: text } }) });

      return result.network.numRequests;
    }
  },

  populateMemoryData(result, data, settings) {
    if (result.mem) {
      const { scale, text } = getCorrectScales(settings, true);

      const virtualMemData = []; const mappedMemData = []; const residentMemData = [];
      const time = new Date().getTime();

      virtualMemData.push([time, Math.round((result.mem.virtual * scale) * 100) / 100]);
      mappedMemData.push([time, Math.round((result.mem.mapped * scale) * 100) / 100]);
      residentMemData.push([time, Math.round((result.mem.resident * scale) * 100) / 100]);


      data.push({ data: virtualMemData, label: Helper.translate({ key: 'virtual' }) });
      data.push({ data: mappedMemData, label: Helper.translate({ key: 'mapped' }) });
      data.push({ data: residentMemData, label: Helper.translate({ key: 'current' }) });

      return text;
    }
  },

  convertInformationsToCorrectUnit(stats, settings) {
    const { scale, text } = getCorrectScales(settings);

    stats.dataSize = Number.isNaN(Number(stats.dataSize / scale)) ? `0 ${text}` : `${Number(stats.dataSize / scale).toFixed(2)} ${text}`;
    stats.storageSize = Number.isNaN(Number(stats.storageSize / scale)) ? `0 ${text}` : `${Number(stats.storageSize / scale).toFixed(2)} ${text}`;
    stats.indexSize = Number.isNaN(Number(stats.indexSize / scale)) ? `0 ${text}` : `${Number(stats.indexSize / scale).toFixed(2)} ${text}`;
    stats.fileSize = Number.isNaN(Number(stats.fileSize / scale)) ? `0 ${text}` : `${Number(stats.fileSize / scale).toFixed(2)} ${text}`;
  },

  initOperationCountersChart(data) {
    const customOptions = $.extend(true, {}, this.lineOptions);
    customOptions.colors = [];
    customOptions.bars = {
      align: 'center',
      barWidth: 0.5,
    };
    customOptions.series = {
      bars: {
        show: true,
      },
      points: {
        show: true,
      },
    };
    customOptions.xaxis = {
      show: true,
      ticks: [[0, 'Insert'], [1, 'Query'], [2, 'Update'], [3, 'Delete'], [4, 'Getmore']],
    };

    this.opCountersChart = initChart.call(this, {
      chartVariable: this.opCountersChart,
      divSelector: $('#divOperationCountersChart'),
      data,
      merge: false,
      lineOptions: customOptions,
    });
  },

  initQueuedReadWriteChart(data, totalQueuedReadWrite) {
    this.queuedReadWriteChart = initChart.call(this, {
      chartVariable: this.queuedReadWriteChart,
      spanSelector: $('#spanTotalQueuedRW'),
      divSelector: $('#divQueuedReadWrite'),
      data,
      total: totalQueuedReadWrite,
      lineOptions: $.extend(true, {}, this.lineOptions),
    });
  },

  initActiveReadWriteChart(data, totalActiveReadWrite) {
    this.activeReadWriteChart = initChart.call(this, {
      chartVariable: this.activeReadWriteChart,
      spanSelector: $('#spanTotalActiveRW'),
      divSelector: $('#divActiveReadWrite'),
      data,
      lineOptions: $.extend(true, {}, this.lineOptions),
      total: totalActiveReadWrite
    });
  },

  initNetworkChart(data, totalRequests) {
    this.networkChart = initChart.call(this, {
      chartVariable: this.networkChart,
      spanSelector: $('#spanTotalRequests'),
      divSelector: $('#divNetworkChart'),
      data,
      total: totalRequests,
      lineOptions: $.extend(true, {}, this.lineOptions),
      translateKey: 'total_requests'
    });
  },

  initConnectionsChart(data, availableConnections) {
    this.connectionsChart = initChart.call(this, {
      chartVariable: this.connectionsChart,
      spanSelector: $('#spanAvailableConnections'),
      divSelector: $('#divConnectionsChart'),
      data,
      total: availableConnections,
      lineOptions: this.lineOptions,
      translateKey: 'available'
    });
  },

  initMemoryChart(data, text) {
    const customLineOptions = $.extend(true, {}, this.lineOptions);
    customLineOptions.colors.push('#273be2');
    customLineOptions.yaxis = {
      tickFormatter(val) {
        return `${val} ${text}`;
      },
    };

    this.memoryChart = initChart.call(this, {
      chartVariable: this.memoryChart,
      divSelector: $('#divHeapMemoryChart'),
      data,
      lineOptions: customLineOptions
    });
  }
};

export default new DBStats();