Name

sn_app_insights.MetricDataRetriever

Description

No description available

Script

var MetricDataRetriever = Class.create();
MetricDataRetriever.prototype = {
  initialize: function() {
      this.X_AXIS = "x";
      this.X_AXIS_KEY = "timeStamp";
      this.Y_AXIS = "y";
      this.TYPE = "type";
      this.LINE_TYPE = "line";
      this.SCATTER_TYPE = "scatter";
      this.LABEL = "label";
      this.LINE_CONFIG_KEY = "line";
      this.AVG_CONFIG_KEY = "1-day-avg";
      this.SYS_CLUSTER_STATE = "sys_cluster_state";
      this.ECC_QUEUE_STATS_BY_ECC_AGENT = "ecc_queue_stats_by_ecc_agent";
      this.EXCLUSION_GROUPING_LIST = [this.AVG_CONFIG_KEY];
  },

  /**
   * @param secondsInTimeWindow: The number of seconds in the past for the time window. For example, for 604800
   * the payload would contain 7 days worth of data.
   *
   * @param tableName: The table name the metric subject(s) resides in. For example, sys_pattern for slow patterns.
   * Required because MetricBase enforces uniqueness based on table name and metric name, not just metric name.
   *
   * @param metricName: The metric name for the requested metric values. For example, execution_count for slow patterns.
   *
   * @returns A JSON payload containing metric values over a 7 day period for n lines and a config containing
   * metadata for each line. The payload is formatted to be consumed by the seismic timeseries chart component. For
   * example, this means that each point in time's timeStamp is in EPOC seconds and not milliseconds.
   * An example payload:
   {
    "seriesConfig": {
      "line0": {
        "label": "192.168.1.8:paris",
        "type": "line",
        "x": "timeStamp",
        "y": "y0"
      },
      "line1": {
        "label": "192.168.1.8:paris2",
        "type": "line",
        "x": "timeStamp",
        "y": "y1"
      }
    },
    "seriesData": [
      {
        "timeStamp": 1603473000,
        "y0": 37.70872116088867,
        "y1": 36.847618103027344
      },
      {
        "timeStamp": 1603473300,
        "y0": 36.0965690612793,
        "y1": 35.09318161010742
      },
      {
        "timeStamp": 1603473600,
        "y0": 35.38829040527344,
        "y1": 34.14033126831055
      }
    ]
   }
   */
  getMetricValuesPayload: function(secondsInTimeWindow, tableName, metricName, filter, plotType) {
      // 7 days in seconds
      var end = new GlideDateTime();
      var start = new GlideDateTime(end);
      start.addSeconds(-1 * secondsInTimeWindow);

      // query subject records
      var metricSubjects = new GlideRecord(tableName);
      if (tableName == this.SYS_CLUSTER_STATE)
          metricSubjects.addQuery("status", "online");

      if (filter)
          metricSubjects.addEncodedQuery(filter);

      metricSubjects.query();
  	
      if (metricSubjects.getRowCount() <= 0)
          return {
              'seriesConfig': {},
              'seriesData': [],
              'thresholdData': {}
          };

      /**
       * There is currently a bug in MetricBase that returns data at a 1 second period
       * instead of the period configured in the retention policy. This then makes the
       * data array returned by MetricBase filled with a significant number of NaNs. So,
       * to workaround this, we resample the data to fit a period we specify. If we're
       * getting node metrics, the period for the scheduled job that persists node metrics
       * is used. Otherwise, we use a default 5 minute period.
       */
      var resamplePeriodDuration;
      if (tableName == this.SYS_CLUSTER_STATE) {
          var scheduledJob = new GlideRecord("sysauto_script");
          scheduledJob.get("8c94b336c37310107f5633f6bb40dddc");
          resamplePeriodDuration = new GlideDuration(scheduledJob.run_period.dateNumericValue());
      } else {
          resamplePeriodDuration = new GlideDuration(5 * 60 * 1000);
      }

      var transformer = new sn_clotho.Transformer(metricSubjects);
      transformer.metric(metricName).resample("AVG", resamplePeriodDuration);

      // execute and return result for visualizing
      var resultArray = transformer.execute(start, end).toArray();

      //Build threshold data before calculating moving average so moving average does not skew threshold
      var payload = {};
      var thresholdUtils = new ThresholdUtils();
      payload["thresholdData"] = thresholdUtils.buildThresholdData(resultArray, metricName, tableName);
      payload["thresholdConfig"] = thresholdUtils.buildThresholdConfig();

      //Get new TransformPart for moving average and concatenate to result array
      transformer = new sn_clotho.Transformer(metricSubjects);
      transformer
          .metric(metricName)
          .resample("AVG", resamplePeriodDuration)
          .filter("AVG", new GlideDuration(24 * 60 * 60 * 1000))
          .avg(); // Need this last average to turn it into a single line for a moving average

      resultArray = resultArray.concat(transformer.execute(start, end).toArray());

      //Used to define aggregate labels and at which index they are in resultArray
      var aggregateIndices = {};
      aggregateIndices[resultArray.length - 1] = gs.getMessage('1-Day Moving Average');

      //Build config with all data
      payload["seriesConfig"] = this._buildSeriesConfigForGraph(resultArray, aggregateIndices, plotType);
      payload["seriesData"] = this._buildSeriesDataWithMetricValues(resultArray);
      var annotationsDataUtils = new AnnotationsDataUtils();
      payload["annotationsConfig"] = annotationsDataUtils._getAnnotationsConfig();

      var annotationsData = annotationsDataUtils._getAnnotationsData(start);
      payload["annotationsData"] = annotationsData.data;
      payload["annotationsMetadata"] = annotationsData.counts;

      return payload;
  },

  getClusteredMetricValuesPayload: function(secondsInTimeWindow, tableName, metricName, filter, plotType) {
  	var originalPayload = this.getMetricValuesPayload(secondsInTimeWindow, tableName, metricName, filter, plotType);
      if (!this.canCluster(originalPayload))
          return originalPayload;

      var kmeanClusters = new Kmeans();
      var clusteredPayload = kmeanClusters.processAndClusterData(originalPayload, this.EXCLUSION_GROUPING_LIST);
      return clusteredPayload;
  },

  canCluster: function(originalPayload) {
      var minNumberOfLinesToGroup = gs.getProperty('glide.appinsights.min_number_of_lines_to_group', 7);
      if (Object.keys(originalPayload["seriesConfig"]).length > minNumberOfLinesToGroup)
          return true;

      return false;
  },

  /**
   * @private
   * @param resultArray The result array returned by MetricBase. For example, this array would contain all
   * metric values across all nodes
   *
   * @returns The seriesConfig object required by a seismic time series chart. Example:
   *
   {
        node0: { x: "timeStamp", y:"y0", type: "line", label: "node0SystemId" },
        node1: { x: "timeStamp", y:"y1", type: "line", label: "node1SystemId" }
   }
   */
  _buildSeriesConfigForGraph: function(resultArray, aggregateIndices, plotType) {
      var seriesConfig = {};
      for (var i = 0; i < resultArray.length; i++) {
          var singleLineConfig = {};
          singleLineConfig[this.X_AXIS] = this.X_AXIS_KEY;
          singleLineConfig[this.Y_AXIS] = this.Y_AXIS + i;
          singleLineConfig[this.TYPE] = this.LINE_TYPE;
          if (aggregateIndices && aggregateIndices.hasOwnProperty(i)) {
              singleLineConfig[this.LABEL] = aggregateIndices[i];
              singleLineConfig[this.TYPE] = this.LINE_TYPE;
              seriesConfig[this.AVG_CONFIG_KEY] = singleLineConfig;
              continue;
          } else {
              var tableName = resultArray[i].getTableName();
              singleLineConfig[this.LABEL] = this._getLineLabelForSeriesConfig(tableName, resultArray[i].getSubject());
              singleLineConfig[this.TYPE] = plotType;
          }

          seriesConfig[this.LINE_CONFIG_KEY + i] = singleLineConfig;
      }

      return seriesConfig;
  },

  _getLineLabelForSeriesConfig: function(tableName, subjectSysId) {
      var gr = new GlideRecord(tableName);
      if (!gr.get(subjectSysId))
          return "";

      if (tableName == this.ECC_QUEUE_STATS_BY_ECC_AGENT)
          return gr.getValue("agent");

      return gr.getValue(gr.getDisplayName());
  },

  /**
   * @private
   * @param resultArray The array of metric values for n lines returned by MetricBase
   *
   * @returns An array of JSON objects where each object contains the metric values, for all lines, at a point in time.
   * An example of the return array of JSON objects:
   [
   {
        "timeStamp": 1603473000,
        "y0": 37.70872116088867,
        "y1": 36.847618103027344
      },
   {
        "timeStamp": 1603473300,
        "y0": 36.0965690612793,
        "y1": 35.09318161010742
      },
   {
        "timeStamp": 1603473600,
        "y0": 35.38829040527344,
        "y1": 34.14033126831055
      }
   ]
   */
  _buildSeriesDataWithMetricValues: function(resultArray) {
      if (resultArray.length == 0)
          return [];

      var seriesData = this._populateSeriesDataSkeletonWithTimeStamps(resultArray[0]);
      for (var lineNumber = 0; lineNumber < resultArray.length; lineNumber++) {
          var valuesOfSingleLine = resultArray[lineNumber].getValues();
          for (var dataPointIndex = 0; dataPointIndex < seriesData.length; dataPointIndex++) {
              var pointInTime = seriesData[dataPointIndex];

              // The NaN object gets turned to a string when it exists in the return, but null doesn't. So
              // make all NaN objects null objects so the front-end can process it correctly.
              var metricValue = valuesOfSingleLine[dataPointIndex];
              pointInTime[this.Y_AXIS + lineNumber] = isNaN(metricValue) ? null : valuesOfSingleLine[dataPointIndex];
          }
      }

      return seriesData;
  },

  /**
   * @private
   * @param singleResultArray An array of metric values for a single line.
   *
   * @returns An array of JSON objects that only contains the time stamps based on the time period of the result and
   * the number of results in the 0th line.
   *
   * NOTE: If the retrieved metric data contains more than one line, this method assumes that all lines have the same
   * number of values as the 0th line. This assumption was made because we have MetricBase resample the data to a
   * set time period and therefore there should only be a single value per point in time.
   *
   * An example of the returned array of JSON objects
   [
   {
        "timeStamp": 1603473000
      },
   {
        "timeStamp": 1603473300
      },
   {
        "timeStamp": 1603473600
      }
   ]
   */
  _populateSeriesDataSkeletonWithTimeStamps: function(singleResultArray) {
      // Create new gdt so we don't modify the original in the result
      var indexDateTime = new GlideDateTime(singleResultArray.getStart());
      var seriesDataSkeleton = [];
      var valueArray = singleResultArray.getValues();
      for (var i = 0; i < valueArray.length; i++) {
          var localTimestamp = new GlideDateTime();
          localTimestamp.setValue(indexDateTime.getDisplayValueInternal());
          seriesDataSkeleton.push({
              "timeStamp": localTimestamp.getNumericValue()
          });

          // Each value in the valueArray is separated from the previous value by an amount
          // of time (ex: 300 seconds), which is the time period. So to get the time stamp for the
          // next value, we need to increment by the period to get the time stamp related to the value
          indexDateTime.addSeconds(singleResultArray.getPeriod());
      }

      return seriesDataSkeleton;
  },

  type: 'MetricDataRetriever'
};

Sys ID

a48ae8e1c30820107f5633f6bb40dd56

Offical Documentation

Official Docs: