Name

sn_em_ai.EvtMgmtProcessLogAnalyticsJson

Description

No description available

Script

var EvtMgmtProcessLogAnalyticsJson = Class.create();
EvtMgmtProcessLogAnalyticsJson.prototype = {
  initialize: function() {
      // Severities map
      this.severityMap = {
          LOW: '4',
          MEDIUM: '3',
          HIGH: '2',
          CRITICAL: '1'
      };

      this.GROUP_TYPES = {
          INCIDENT_GROUP_TYPE: 9,
          TOP_ALERT_GROUP_TYPE: 10,
      };

      this.ALERT_CATEGORY = {
          LOG_ANALYTICS_GROUP: "1",
          LOG_ANALYTICS_ALERT: "2",
          LOG_ANALYTICS_READ_ONLY: "3"
      };

      this.ALLOW_USER_TO_SET_STATE = {
          ANY: "1",
          CLOSE: "2",
          NONE: "3"
      };

      this.TAGS = {
          HIGHLIGHT_OPEN: "<highlight>",
          HIGHLIGHT_CLOSE: "</highlight>",
          STACKTRACE_OPEN: "<stacktrace>",
          STACKTRACE_CLOSE: "</stacktrace>"
      };

      this.APP_SERVICE_TABLE = "cmdb_ci_service_auto";
      this.EXTRA_DATA_TABLE = "em_alert_extra_data";

      this.properties = {
          LA_PREVENT_GROUPING_IDENTICAL_HOSTS: 'evt_mgmt.log_analytics.prevent_grouping_identical_hosts',
          LA_PREVENT_GROUPING_IDENTICAL_HOSTS_DEFAULT_VALUE: 'false',
      };
  },

  type: 'EvtMgmtProcessLogAnalyticsJson',

  process: function(json) {
      var services = "";
      var servicesMap = {};
      this.correlatorsMap = {};
      var minSeverity = 4;
      var topLevelExtraData = {};
      var topLevelNode = "";
      var differentHosts = false;

      if (json && this.isValidTag(json, 'incident') && this.isValidTag(json.incident, 'topAlerts') && json.incident.topAlerts instanceof Array) {
          var incident = json.incident;
          var alertData;
          var children = [];
          for (i = 0; i < incident.topAlerts.length; i++) {
              alertData = incident.topAlerts[i];

              var child = {
                  additional_info: {}
              };
              var extraData = {};
              var appService = this.getApplicationService(alertData);
              var occultusProperties = this.getOccultusProperties(alertData);
              extraData.component = this.setComponent(appService.service);
              extraData.application_service = this.findAppService(appService.application);
              var entitiesData = this.getEntities(alertData);
              extraData.meaningful_entities = entitiesData ? JSON.stringify(entitiesData) : "";
              extraData.anomaly = this.buildAnomalyText(alertData, occultusProperties);
              extraData.explanation = this.getIdentifiedIssueHint(alertData);
              extraData.correlators = JSON.stringify(this.getCorrelators(alertData));

              child.extraData = extraData;
              child.sn_component = extraData.component;
              child.sn_fallback_binding_ci_id = extraData.application_service;
              child.sn_application_service_name = appService.application;
              child.sn_metric_type = alertData.metric.type;
              child.sn_metric_query = alertData.metric.luceneQuery;

              if (alertData.indexPatternId) {
                  child.sn_index_pattern_id = alertData.indexPatternId;
              }

              child.id = alertData.id;
              child.metric_name = alertData.name;
              child.resource = appService.application + "-" + appService.service;
              child.type = alertData.type;
              child.time_of_event = alertData.occurrenceTime;

              // Pattern, if exists
              if (alertData.hasOwnProperty("pattern") && alertData.pattern) {
                  child.sn_pattern_text = alertData.pattern.patternText;
                  child.sn_pattern_external_id = alertData.pattern.patternExternalId;
                  child.sn_pattern_id_per_source = alertData.pattern.idPerSource;
              } else if (alertData.metric.type == "KeywordMetric" || alertData.metric.type == "PatternSeverityMetric") {
                  var decomposedPattern = this.getHighestFreqDecomposedPattern(alertData);
                  if (decomposedPattern != null && decomposedPattern.pattern != null) {
                      child.sn_pattern_text = decomposedPattern.pattern.patternText;
                      child.sn_pattern_id_per_source = decomposedPattern.pattern.idPerSource;
                      child.sn_pattern_external_id = decomposedPattern.pattern.patternExternalId;
                  }
              }

              // severity handling
              child.severity = this.severityMap[alertData.severity];
              if (parseInt(child.severity, 10) < minSeverity) {
                  minSeverity = parseInt(child.severity, 10);
              }

              // application service for incident level
              var app = alertData.application;
              if (app && !servicesMap.hasOwnProperty(app)) {
                  // new service
                  servicesMap[app] = 1;
                  if (i == 0) {
                      services += app;
                  } else {
                      services += ", " + app;
                  }
              }

              // description calculation
              var desc_obj = this.getDescriptions(alertData, child);
              child.description = desc_obj.description;
              // we would save the tagged description according to the property
              var saveInTable = gs.getProperty('evt_mgmt.save_formatted_description_in_extra_data_table', "false");
              if (saveInTable == 'true') {
                  // save it in the em_alert_extra_data table
                  extraData.tagged_description = desc_obj.tagged_description;
              } else {
                  // save it in as part of the additional_info field
                  child.sn_tagged_description = desc_obj.tagged_description;
              }
              // special case for log analytics short description
              child.additional_info.sn_short_desc_override =
                  this.getKBSearchInfo(alertData, child.sn_pattern_text, child.sn_pattern_external_id, true, child.description);

              child.additional_info.sn_kb_meta_data =
                  this.getKBSearchInfo(alertData, child.sn_pattern_text, child.sn_pattern_external_id, false, child.description);

              // Anomaly data
              if (alertData.metric && alertData.metric.properties) {
                  child.sn_metric_subject = alertData.metric.properties.subject;
                  child.sn_metric_dimension = alertData.metric.properties.dimension;
                  child.sn_metric_specific_type = alertData.metric.properties.type;
              }
              child.sn_metric_based_alert = "true";
              child.sn_detection_type = alertData.detectionType;
              child.sn_anomaly_current = alertData.currentValue;
              child.sn_anomaly_expected = alertData.expectedValue;
              child.sn_anomaly_change_percentage = alertData.changePercentage;
              child.sn_points_in_timeless_trend = occultusProperties.points_in_timeless_trend;
              child.sn_recent_events_period_seconds = occultusProperties.recent_events_period_seconds;
              child.sn_custom_time = occultusProperties.customTime;
              child.sn_custom_threshold = occultusProperties.customThreshold;
              child.sn_custom_number = occultusProperties.customNumber;
              child.sn_operator = occultusProperties.operator;

              // hosts data
              var hostsData = this.getHostsData(alertData);


              var grandchildren = [];

              // check special case when there is only 1 CI in this alert
              if (Object.keys(hostsData).length == 1) {
                  child.node = this.findFqdnCi(Object.keys(hostsData)[0]);

                  // now, let's check if this is the same host across all alerts in this incident
                  if (!differentHosts) {
                      if (topLevelNode == "") {
                          topLevelNode = child.node; // assigning topLevelNode first time
                      } else {
                          if (topLevelNode != child.node) {
                              topLevelNode = ""; // different hosts in alerts, no host will be in the incident level
                              differentHosts = true;
                          }
                      }
                  }
              } else { // multiple hosts in this alert
                  differentHosts = true;
                  topLevelNode = ""; // different hosts in alerts, no host will be in the incident level

                  for (h = 0; h < Object.keys(hostsData).length; h++) {
                      var grandchild = JSON.parse(JSON.stringify(child));
                      grandchild.node = Object.keys(hostsData)[h];
                      if (!grandchild.node.localeCompare("Others"))
                          continue; // ignore "Others"
                      grandchild.node = this.findFqdnCi(grandchild.node);
                      grandchild.id = child.id + "_" + grandchild.node;
                      grandchildren.push(grandchild);
                  }
                  child.children = grandchildren;
              }
              children.push(child);
          }

          var description = gs.getMessage("Group of alerts, issue identified in {0}", [services]);
          var severity; // for incident level
          if (this.isValidTag(incident, "severity")) {
              severity = this.severityMap[incident.severity];
          } else {
              severity = minSeverity.toString();
          }

          if (services && !services.includes(",")) {
              topLevelExtraData.application_service = this.findAppService(services);
          }

          var updatedCorrelatorsMap = this.removeSingleAlertCorrelators();
          topLevelExtraData.correlators = JSON.stringify(updatedCorrelatorsMap);

          return {
              tree: {
                  id: incident.sysId,
                  description: description,
                  time_of_event: incident.occurrenceTime,
                  severity: severity,
                  node: topLevelNode,
                  extraData: topLevelExtraData,
                  children: children,
                  additional_info: {
                      sn_short_desc_override: '', // Always empty string for the incident alert
                      sn_fallback_binding_ci_id: topLevelExtraData.application_service
                  }
              },
          };

      } else {
          return {};
      }
  },

  // find FQDN non-Retired CI for this host
  findFqdnCi: function(host) {
      if (gs.getProperty('evt_mgmt.log_analytics.look_for_fqdn_ci', 'true') == 'false') // option to disable this feature
          return host;

      if (!host || host.indexOf(".") >= 0)
          return host; // host is empty or is already in FQDN format

      var checkRetired = gs.getProperty('evt_mgmt.ignore_retired_cis_in_binding', 'true');
      if (checkRetired === 'true') {
          var ignoredStatuses = gs.getProperty('evt_mgmt.install_status_list_to_ignore_in_binding');
          var ignoredStatusesList = ignoredStatuses.split(',');
      }

      var gr = new GlideRecord("cmdb_ci_hardware");
      gr.addQuery('name', 'STARTSWITH', host + ".");
      gr.query();

      var count = 0;
      var name = "";
      while (gr.next()) {
          var state = gr.getValue("install_status");
          if (ignoredStatusesList.indexOf(state) >= 0)
              continue; // ignore Retired CI

          count++;
          if (count > 1)
              break; // many results - no FQDN fix, returning original host 

          name = gr.getValue("name");
      }

      if (count == 1)
          return name;

      return host; // no FQDN results or many results
  },

  setComponent: function(service) {
      if (service && service != 'all services') {
          return service;
      }

      //special case, when there is all services there is no need to filter the surrounding logs, so there is need to put All components here.
      return "All components";
  },

  concatenateParts: function(parts) {
      var result = "",
          hl_message = "";
      for (h = 0; h < parts.length; h++) {
          if (parts[h].value) {
              result += parts[h].value;
              if (parts[h].examples && parts[h].examples.length > 0) {
                  hl_message += this.highlightTag(parts[h].value);
              } else {
                  hl_message += parts[h].value;
              }
          }
      }
      return {
          description: result,
          tagged_description: hl_message
      };
  },

  // find decomposed pattern with the highest "frequency" and return the relevant json element
  getHighestFreqDecomposedPattern: function(alertData) {
      if (this.isValidTag(alertData, "decomposedFrequentPatterns") && alertData.decomposedFrequentPatterns.length > 0) {
          var patterns = alertData.decomposedFrequentPatterns;
          var maxFreq = 0;
          var maxIndex = 0;
          for (h = 0; h < patterns.length; h++) {
              if (patterns[h].frequency > maxFreq) {
                  maxFreq = patterns[h].frequency;
                  maxIndex = h;
              }
          }
          return patterns[maxIndex].decomposedPattern;
      }
      return null;
  },

  // find decomposed pattern with the highest "frequency" and return the relevant message
  highestFreqPattern: function(alertData) {
      var decomposedPattern = this.getHighestFreqDecomposedPattern(alertData);
      if (decomposedPattern != null && decomposedPattern.parts && decomposedPattern.parts.length > 0) {
          parts = decomposedPattern.parts;
          //TODO: Should be concatenated like a HOLMES message to set a limit on the length of the message (see getDescription)
          return this.concatenateParts(parts);
      } else {
          var service = alertData.metric.properties.service;
          //trend description can be "above normal / below normal / anomalous"
          var trendDescription = this.getAnomalyTrendDescription(alertData.detectionType);
          var message = '',
              hl_message = '';
          switch (alertData.metric.type) {
              case "KeywordMetric":
                  var keyword = alertData.metric.properties.keyword;
                  message = gs.getMessage("The total volume of Keyword '{0}' from '{1}' is {2}", [keyword, service, trendDescription]);
                  hl_message = gs.getMessage("The total volume of Keyword {0}{2}{1} from '{3}' is {4}", [this.TAGS.HIGHLIGHT_OPEN, this.TAGS.HIGHLIGHT_CLOSE, keyword, service, trendDescription]);
                  break;

              case "PatternSeverityMetric":
                  var severity = alertData.metric.properties.severity;
                  message = gs.getMessage("The total volume of {0} level logs from '{1}' is {2}", [severity, service, trendDescription]);
                  hl_message = gs.getMessage("The total volume of {0}{2}{1} level logs from '{3}' is {4}", [this.TAGS.HIGHLIGHT_OPEN, this.TAGS.HIGHLIGHT_CLOSE, severity, service, trendDescription]);
                  break;
          }
          return {
              description: message,
              tagged_description: hl_message

          };
      }
  },

  // find application service by name
  findAppService: function(name) {
      var gr = new GlideRecord(this.APP_SERVICE_TABLE);
      gr.addQuery("name", name);
      gr.query();
      if (gr.next()) {
          return gr.getValue("sys_id"); // GetValue()
      }
      return null;
  },

  // check if the JSON tag/key is valid
  isValidTag: function(element, tag) {
      return element.hasOwnProperty(tag) && element[tag];
  },

  // get hostsData tag
  getHostsData: function(alertData) {
      var hostsData = {};
      entitiesData = this.getEntities(alertData);
      if (entitiesData) {
          for (h = 0; h < entitiesData.length; h++) {
              var entity = entitiesData[h];
              if (entity.entity.toLowerCase() == "host") {
                  hostsData = alertData.rootCauseAnalysis.entities.entities[h].distribution.relativeParts;
                  break;
              }
          }
      }
      return hostsData;
  },

  // get entities
  getEntities: function(alertData) {
      if (this.isValidTag(alertData, "rootCauseAnalysis") && this.isValidTag(alertData.rootCauseAnalysis, "entities") && this.isValidTag(alertData.rootCauseAnalysis.entities, "entities")) {
          return alertData.rootCauseAnalysis.entities.entities;
      }
      return null;
  },

  getApplicationService: function(alertData) {
      var res = {};
      if (alertData.metric.type == 'CustomMetric') {
          res['application'] = alertData.manualAlert.query.application ? alertData.manualAlert.query.application : "All components";
          if (res['application'] != 'All components') {
              res['service'] = alertData.manualAlert.query.service ? alertData.manualAlert.query.service : "all services";
          } else {
              res['service'] = "";
          }
      } else {
          res['application'] = alertData.application;
          res['service'] = alertData.service;

      }
      return res;
  },

  // construct both event descriptions: regular and hilghlited according to use case
  getDescriptions: function(alertData, child) {
      var description, tag_description;
      var desc_obj = {};
      var descriptionMaxSize = gs.getProperty('evt_mgmt.description_to_highlight_max_size', 3000);
      if (alertData.detectionType == 'SIGNAL_DEAD') {
          description = gs.getMessage("No data is streaming from '{0}'", [alertData.metric.properties.service]);
      } else {
          //trend description can be "above normal / below normal / anomalous"
          var trendDescription = this.getAnomalyTrendDescription(alertData.detectionType);
          switch (alertData.metric.type) {
              case "PatternIdMetric":
                  var parts = alertData.decomposedPattern.parts;
                  desc_obj = this.concatenateParts(parts);
                  description = desc_obj.description;
                  tag_description = desc_obj.tagged_description;
                  break;
              case "RawMetric":
                  var props = alertData.metric.properties;
                  switch (props.type) {
                      case "METER":
                          description = gs.getMessage("The volume of events with '{0} - {1}' is {2}", [props.tag, props.value, trendDescription]);
                          tag_description = gs.getMessage("The volume of events with {0}{2}{1} - {0}{3}{1} is {4}", [this.TAGS.HIGHLIGHT_OPEN, this.TAGS.HIGHLIGHT_CLOSE, props.tag, props.value, trendDescription]);
                          break;
                      case "GAUGE":
                          description = gs.getMessage("The value of '{0}' is {1}", [props.tag, trendDescription]);
                          tag_description = gs.getMessage("The value of {0}{2}{1} is {3}", [this.TAGS.HIGHLIGHT_OPEN, this.TAGS.HIGHLIGHT_CLOSE, props.tag, trendDescription]);
                          break;
                      case "TIMELESSGAUGE":
                          description = gs.getMessage("The value of '{0}' is {1}", [props.tag, trendDescription]);
                          tag_description = gs.getMessage("The value of {0}{2}{1} is {3}", [this.TAGS.HIGHLIGHT_OPEN, this.TAGS.HIGHLIGHT_CLOSE, props.tag, trendDescription]);
                          break;
                  }
                  break;
              case "HistogramMetric":
                  description = gs.getMessage("The relative propotion of the following value is {0}", [trendDescription]);
                  break;
              case "CustomMetric":
                  description = alertData.manualAlert.name;
                  break;

              case "AllEventsMetric":
                  switch (alertData.detectionType) {
                      case "SIGNAL_DEAD":
                          description = gs.getMessage("No data is streaming from '{0}'", [alertData.metric.properties.service]);
                          break;
                      case "ANOMALY_BASELINE_REFERENCE_INCREASE":
                      case "ANOMALY_ABOVE_AVERAGE":
                          description = gs.getMessage("The total volume of events from '{0}' is above normal", [alertData.metric.properties.service]);
                          tag_description = gs.getMessage("The total volume of events from {0}{2}{1} is above normal", [this.TAGS.HIGHLIGHT_OPEN, this.TAGS.HIGHLIGHT_CLOSE, alertData.metric.properties.service]);
                          break;
                      case "ANOMALY_BASELINE_REFERENCE_DECREASE":
                      case "ANOMALY_BELOW_AVERAGE":
                          description = gs.getMessage("The total volume of events from '{0}' is below normal", [alertData.metric.properties.service]);
                          tag_description = gs.getMessage("The total volume of events from {0}{2}{1} is below normal", [this.TAGS.HIGHLIGHT_OPEN, this.TAGS.HIGHLIGHT_CLOSE, alertData.metric.properties.service]);
                          break;

                      default:
                          description = gs.getMessage("The total volume of events from '{0}' is anomalous", [alertData.metric.properties.service]);
                          tag_description = gs.getMessage("The total volume of events from {0}{2}{1} is anomalous", [this.TAGS.HIGHLIGHT_OPEN, this.TAGS.HIGHLIGHT_CLOSE, alertData.metric.properties.service]);
                  }
                  break;

              case "KeywordMetric":
              case "PatternSeverityMetric":
                  if (this.hasHolmesProperty(alertData)) {
                      var holmes = alertData.holmes;
                      var activePart;
                      description = "";
                      tag_description = "";
                      var before = "",
                          after = "";
                      for (h = 0; h < holmes.length; h++) {
                          if (holmes[h].holmesPart) {
                              // found the active part
                              if (h > 0) {
                                  var beforeChar = holmes[h - 1].partText.slice(-46, -45);
                                  before = holmes[h - 1].partText.slice(-45);
                                  if (beforeChar != " " && before.length > 34) {
                                      // we are in the middle of a word. Detect the next whole word, don't show part of a word
                                      var spaceIndex = before.indexOf(' ');
                                      if (spaceIndex < 10) {
                                          before = before.slice(spaceIndex + 1);
                                      }
                                  }
                              }
                              if (h + 1 < holmes.length) {
                                  after = holmes[h + 1].partText.slice(0, 45);
                              }
                              description += "..." + before + holmes[h].partText + after;
                              tag_description += '...' + before + this.highlightTag(holmes[h].partText) + after;
                          }
                      }
                      description += "...";
                      tag_description += "...";
                  } else {
                      desc_obj = this.highestFreqPattern(alertData);
                      description = desc_obj.description;
                      tag_description = desc_obj.tagged_description;
                  }
                  break;
          }

          if (alertData.hasOwnProperty("properties") && alertData.properties &&
              alertData.properties.hasOwnProperty("stacktrace") && alertData.properties.stacktrace) {
              var stacktrace = alertData.properties.stacktrace;
              if (tag_description == "")
                  tag_description = description;
              description += "\n\nStacktrace: \n" + this.TAGS.STACKTRACE_OPEN + stacktrace + this.TAGS.STACKTRACE_CLOSE;
              // in case tag_description is empty, only in stacktrace cases we will copy the description to the tag_description and add the stacktrace tage
              // in the future - stacktrace tags will appeare only in tag_description

              // we want to verify that the tag_description field will not exceed the max size (default 3000) once the stacktrace is added
              // so we calculate its length plus the added tags length and slice the stacktrace to fit the size
              var tag_desc_length = tag_description.length + 50;
              if (stacktrace.length > descriptionMaxSize - tag_desc_length)
                  stacktrace = stacktrace.slice(0, descriptionMaxSize - tag_desc_length);
              tag_description += "\n\nStacktrace: \n" + this.TAGS.STACKTRACE_OPEN + stacktrace + this.TAGS.STACKTRACE_CLOSE;
          }
      }
      // In order to determin whether to highlight the description or not 
      // we need to verify the decription's length is not higher than 3000 
      // or, if set in property: evt_mgmt_description_to_highlight_max_size not higher than this number
      if (tag_description && tag_description.length > descriptionMaxSize)
          tag_description = "";
      return {
          description: description,
          tagged_description: tag_description
      };
  },

  highlightTag: function(content) {
      return this.TAGS.HIGHLIGHT_OPEN + content + this.TAGS.HIGHLIGHT_CLOSE;
  },

  buildAnomalyText: function(alertData, occultusProperties) {
      var str;

      // we need to present the percentage as a positive value 
      var changePercentage = this.percentageAbsoluteVal(alertData.changePercentage);

      //set maximum 2 digits after the point to numerival vals
      changePercentage = changePercentage <= 1000 ? gs.getMessage("{0}", [this.formatTwo(changePercentage)]) : gs.getMessage("above 1000");
      var currentValue = this.formatTwo(alertData.currentValue);
      var expectedValue = this.formatTwo(alertData.expectedValue);

      var metricType = alertData.metric.properties.type;

      switch (alertData.detectionType) {
          case ('SIGNAL_ALIVE'):
              str = gs.getMessage("{0} Events per minute, Typically inactive", currentValue);
              break;

          case ('ANOMALY_BASELINE_REFERENCE_INCREASE'):
              currentValue = this.formatTwo(parseFloat(alertData.currentValue));
              var anomalyIncreasedDiffValue = alertData.currentValue - alertData.expectedValue;
              expectedValue = this.formatTwo(parseFloat(anomalyIncreasedDiffValue));
              changePercentage = this.formatTwo(((alertData.currentValue / anomalyIncreasedDiffValue) * 100) - 100);
              if (metricType == 'GAUGE') {
                  str = gs.getMessage("Avg. baseline value: {0} Same hour - one week earlier: {1} ({2}% increase)", [currentValue, this.formatTwo(anomalyIncreasedDiffValue), changePercentage]);
              } else {
                  str = gs.getMessage("{0} Events per minute, Same hour - one week earlier: {1}. ({2}% increase)", [currentValue, expectedValue, changePercentage]);
              }
              break;

          case ('ANOMALY_BASELINE_REFERENCE_DECREASE'):
              currentValue = this.formatTwo(parseFloat(alertData.currentValue));
              var anomalyDecreasedDiffValue = alertData.currentValue - alertData.expectedValue;
              expectedValue = this.formatTwo(parseFloat(anomalyDecreasedDiffValue));
              changePercentage = this.formatTwo(((anomalyDecreasedDiffValue - alertData.currentValue) / anomalyDecreasedDiffValue) * 100);
              if (metricType == 'GAUGE') {
                  str = gs.getMessage("{0} Avg. baseline value, Same hour - one week earlier: {1} ({2}% decrease)", [currentValue, this.formatTwo(anomalyDecreasedDiffValue), changePercentage]);
              } else {
                  str = gs.getMessage("{0} Events per minute, Same hour - one week earlier: {1}. ({2}% decrease)", [currentValue, expectedValue, changePercentage]);
              }
              break;

          case ('ANOMALY_STEEP_INCREASE'):
          case ('ANOMALY_ABOVE_AVERAGE'):
              if (metricType == 'TIMELESSGAUGE') {
                  currentValue = this.formatTwo(parseFloat(alertData.currentValue));
                  expectedValue = this.formatTwo(parseFloat(alertData.expectedValue));
                  str = gs.getMessage("{0} Value of anomaly. Avg. of last {1} measurements: ({2}% increase)", [currentValue, expectedValue, changePercentage]);
              } else if (metricType == 'GAUGE') {
                  currentValue = this.formatTwo(parseFloat(alertData.currentValue) * 60);
                  expectedValue = this.formatTwo(parseFloat(alertData.expectedValue) * 60);
                  str = gs.getMessage("{0} Value of anomaly. Avg. of last hour: {1}. ({2}% increase)", [currentValue, expectedValue, changePercentage]);
              } else {
                  currentValue = this.formatTwo(parseFloat(alertData.currentValue) * 60);
                  expectedValue = this.formatTwo(parseFloat(alertData.expectedValue) * 60);
                  str = gs.getMessage("{0} Events per minute Same hour - one day earlier: {1}. ({2}% increase)", [currentValue, expectedValue, changePercentage]);
              }
              break;

          case ('ANOMALY_STEEP_DECREASE'):
          case ('ANOMALY_BELOW_AVERAGE'):
              if (metricType == 'TIMELESSGAUGE') {
                  currentValue = this.formatTwo(parseFloat(alertData.currentValue));
                  expectedValue = this.formatTwo(parseFloat(alertData.expectedValue));
                  str = gs.getMessage("{0} Value of anomaly. Avg. of last {1} measurements: ({2}% decrease)", [currentValue, expectedValue, changePercentage]);
              } else if (metricType == 'GAUGE') {
                  currentValue = this.formatTwo(parseFloat(alertData.currentValue) * 60);
                  expectedValue = this.formatTwo(parseFloat(alertData.expectedValue) * 60);
                  str = gs.getMessage("{0} Value of anomaly. Avg. of last hour: {1}. ({2}% decrease)", [currentValue, expectedValue, changePercentage]);
              } else {
                  currentValue = this.formatTwo(parseFloat(alertData.currentValue) * 60);
                  expectedValue = this.formatTwo(parseFloat(alertData.expectedValue) * 60);
                  str = gs.getMessage("{0} Events per minute Same hour - one day earlier: {1}. ({2}% decrease)", [currentValue, expectedValue, changePercentage]);
              }
              break;

          case ('TIMELESS_TREND_INCREASE'):
              str = gs.getMessage("{0} Value of anomaly. Avg. of last {1} measurements: {2}({3}% Increase) ", [currentValue, occultusProperties.points_in_timeless_trend, expectedValue, changePercentage]);
              break;

          case ('TIMELESS_TREND_DECREASE'):

              str = gs.getMessage("{0} Value of anomaly. Avg. of last {1} measurements: {2}({3}% Decrease) ", [currentValue, occultusProperties.points_in_timeless_trend, expectedValue, changePercentage]);
              break;
          case ('CUSTOM'):
              str = gs.getMessage("Log Entries in the course of {0} {1}: {2} {3} {4} (defined threshold)", [occultusProperties.customNumber, occultusProperties.customTime, currentValue, occultusProperties.operator, occultusProperties.customThreshold]);
              break;
          case ('NEW_SIGNAL'):
              str = gs.getMessage("New behaviour, No data to display at the moment");
              break;

          case ('SIGNAL_DEAD'):
              str = gs.getMessage("No data is streaming from {0}", alertData.service);
              break;

          default:
              str = gs.getMessage("No data, Unrecognized detection type");
      }
      return str;
  },


  getOccultusProperties: function(alertData) {
      var points_in_timeless_trend;
      var recent_events_period_seconds;
      var time;
      var threshold;
      var number;
      var operator;
      if (alertData.detectionType == 'TIMELESS_TREND_DECREASE' ||
          alertData.detectionType == 'TIMELESS_TREND_INCREASE') {
          var gr = new GlideRecord("sn_occ_system_settings");
          gr.addQuery("name", "detective.points_in_timeless_trend");
          gr.query();
          if (gr.next())
              points_in_timeless_trend = gr.getValue("value");

          gr.initialize();
          gr.addQuery("name", "alerts.recent_events_for_timeless_gauge_period_seconds");
          gr.query();
          if (gr.next())
              recent_events_period_seconds = gr.getValue("value");
      }

      if (alertData.detectionType == "CUSTOM") {
          var sysId = alertData.metric.properties.manualAlertSysID;
          var operatorVal;

          var customGr = new GlideRecord("sn_occ_custom_alert");
          customGr.addQuery("sys_id", sysId);
          customGr.query();
          if (customGr.next()) {
              var period = parseInt(customGr.getValue("period"), 10);
              threshold = customGr.getValue("value_count");
              number = customGr.getValue("repetitions_in_period");
              operatorVal = parseInt(customGr.getValue("comparison_operator"), 10);
              if (period == 0) {
                  if (number == 1)
                      time = gs.getMessage("Minute");
                  else
                      time = gs.getMessage("Minutes");
              } else {
                  if (number == 1)
                      time = gs.getMessage("Hour");
                  else
                      time = gs.getMessage("Hours");
              }
              switch (operatorVal) {
                  case 0:
                      operator = ">";
                      break;
                  case 1:
                      operator = "<";
                      break;
                  case 2:
                      operator = "=";
                      break;
                  case 3:
                      operator = ">=";
                      break;
                  case 4:
                      operator = "<=";
                      break;
                  default:
                      operator = ">";
                      break;

              }

          }

      }

      var occultusProperties = {};
      occultusProperties.points_in_timeless_trend = points_in_timeless_trend;
      occultusProperties.recent_events_period_seconds = recent_events_period_seconds;
      occultusProperties.customTime = time;
      occultusProperties.customThreshold = threshold;
      occultusProperties.customNumber = number;
      occultusProperties.operator = operator;

      return occultusProperties;

  },

  getAllowUserToSetState: function(isGog, parentGroupType, myGroupType) {
      // set the sn_allow_setting_state property based on the parent group type
      if (!isGog) {
          return this.ALLOW_USER_TO_SET_STATE.ANY;
      }

      switch (parentGroupType) {
          case this.GROUP_TYPES.INCIDENT_GROUP_TYPE:
              // parent level, means my rights are "any"
              return this.ALLOW_USER_TO_SET_STATE.ANY;
          case this.GROUP_TYPES.TOP_ALERT_GROUP_TYPE:
              // middle level, means my rights are "none"
              return this.ALLOW_USER_TO_SET_STATE.NONE;
          default:
              // I don't have a parent (null); now it depends on my own group type
              if (myGroupType == this.GROUP_TYPES.INCIDENT_GROUP_TYPE) {
                  return this.ALLOW_USER_TO_SET_STATE.CLOSE; // close only
              } else {
                  return this.ALLOW_USER_TO_SET_STATE.ANY; // My group type is top level, so allow "any"
              }
      }
  },

  /**
   * Applies modifications on the finalized tree - i.e., after it was processed to its final structure.
   * 
   * @param tree - the tree that was processed by this LogAnalytics processor
   * @returns
   */
  modifyTreeBeforeCreatingEvents: function(tree) {
      if (!tree) {
          return;
      }

      // The following checks the values of instance level properties and modifies the given tree accordingly.
      // The default behavior of log analytics groups is to group HLA alerts with parent and children, 
      // regardless and independent of their hosts (CIs), in a way it doesn't matter if they have the same host or different hosts.
      //
      // Check the value of the 'evt_mgmt.log_analytics.prevent_grouping_identical_hosts' property,
      // by which treenodes with the same host will not be grouped, so they will be created as standalone alerts.
      // 
      // TreeNodes with different hosts will maintain the default behavior and create a group.
      //
      // TreeNodes with the same host are identified by the "node" field of their parent.
      // - An empty "node" field for the parent treenode (the root node) indicates the tree nodes don't share the same host.
      // - A non-empty "node" field for the parent event (the root node) indicates the tree nodes share the host that the parent "node" field is set to.
      //
      // To prevent HLA alerts from grouping we'll set GOG flag (additional_info key) to false for this treeNode
      // so this will affect the result of the event that's returned from 'getCustomizedEvent(treeNode)' in a way that:
      // 1. sn_partofGOGgroup - will be set to false, to prevent grouping.
      // 2. getManipulatedTimeOfEvent - event time won't be manipulated, as it depends on the sn_partofGOGgroup value.
      // 3. event.sn_allow_setting_state - state could be changed to any, as it depends on the sn_partofGOGgroup value.
      var preventGroupingIdenticalHosts = gs.getProperty(this.properties.LA_PREVENT_GROUPING_IDENTICAL_HOSTS, this.properties.LA_PREVENT_GROUPING_IDENTICAL_HOSTS_DEFAULT_VALUE);
      if (preventGroupingIdenticalHosts === 'true') {
          // If the parent treeNode has a "node" value then its children are sharing the same host.
          // In this case:
          // 1. Don't create the parent node, by setting its createAsEvent flag to false.
          // 2. Set the child tree nodes so they won't be GoG.
          var treeRootNode = tree.getRoot();
          if (treeRootNode.isGog() && treeRootNode.getEventNode()) {
              // Skip on the creation of the tree node
              treeRootNode.setCreateAsEvent(false);

              // Set all children of treeNode as not GoG
              var allTreeNodesArray = tree.toArray();
              for (var i = 0; i < allTreeNodesArray.length; i++) {
                  allTreeNodesArray[i].setGog(false);
              }
          }
      }

      return; // Return, assuming that checking the next properties is an irrelevant check
  },

  getGroupType: function(treeNode) {
      var treeLevel = treeNode.getActualTreeNodeLevel();
      treeLevel = parseInt(treeLevel, 10);

      switch (treeLevel) {
          case 1:
              return this.GROUP_TYPES.INCIDENT_GROUP_TYPE;
          case 2:
              return this.GROUP_TYPES.TOP_ALERT_GROUP_TYPE;
          default:
              return '';
      }
  },

  getManipulatedTimeOfEvent: function(treeNode) {
      // Don't manipulate the time of an alert that its category is "Log Analytics Alert"
      var category = !treeNode.isGog() ? this.ALERT_CATEGORY.LOG_ANALYTICS_ALERT : this.getCategoryByLevel(treeNode.getActualTreeNodeLevel());

      // If the cateogry is not Log Analytics Alert
      switch (category) {
          case this.ALERT_CATEGORY.LOG_ANALYTICS_GROUP:
              var nodeTime = treeNode.getOriginalTime();
              var childTime = '';

              // If there are children then get the minimum time of all children and subtract a second from it
              if (treeNode.hasChildren()) {
                  childTime = treeNode.getChildAt(0).getOriginalTime(); // init with the first child time

                  for (var i = 0; i < treeNode.getChildCount(); i++) {
                      if (treeNode.getChildAt(i).getOriginalTime() < childTime) {
                          childTime = treeNode.getChildAt(i).getOriginalTime();
                      }
                  }
              }
              if (childTime && (childTime <= nodeTime)) {
                  return this.manipulateTime(childTime, 'sub', 1);
              }
              // If there are no children, or the parent already has the earliest time then return time as it
              else {
                  return this.manipulateTime(nodeTime, '', 0);
              }
              case this.ALERT_CATEGORY.LOG_ANALYTICS_ALERT:
                  return this.manipulateTime(treeNode.getOriginalTime(), '', 0);
              case this.ALERT_CATEGORY.LOG_ANALYTICS_READ_ONLY:
                  // Return the parent time of event plus an addition
                  return this.manipulateTime(treeNode.getParent().getOriginalTime(), 'add', 1);
              default:
                  return new GlideDateTime().getValue();
      }
  },

  /**
   * Returns date in format "YYYY-MM-DD HH:MM:SS", for given milliseconds
   * @eventTime - number: milliseconds to get date for
   * @action - str: the action to apply on the given ms, 'add' to add, 'sub' to subtract or '' to leave as is just for the conversion of the format
   * @seconds - number: number of seconds to add or subtract from the given ms
   **/
  manipulateTime: function(eventTime, action, seconds) {
      seconds = parseInt(Number(seconds), 10);
      var isValidSeconds = !isNaN(seconds);
      if (eventTime && isValidSeconds) {
          var milliseconds = seconds * 1000;
          var gdt = new GlideDateTime();
          gdt.setValue(eventTime);
          switch (action) {
              case 'add':
                  gdt.add(milliseconds);
                  break;
              case 'sub':
                  gdt.subtract(milliseconds);
                  break;
              default:
                  break;
          }
          return gdt.getValue();
      }
      return new GlideDateTime().getValue();
  },


  /*
   * Adds data to the event that will be created:
   *   - Keys that will be named as the event table columns, will override existing values.
   *   - Keys that are not part of the event table will be inserted to the event's additional_info field.
   */
  addToEvent: function(event, treeNode) {
      event.sn_allow_setting_state = this.getAllowUserToSetState(treeNode.isGog(), treeNode.getParentGroupType(this), treeNode.getMyGroupType(this));
  },

  /*
   * Adds data to the em_alert_extra_data table.
   *   - Keys must be named as one of the fields in em_alert_extra_data.
   */
  manipulateExtraData: function(treeNode) {
      // Add alert category to extra data table only if part of GOG
      var category = !treeNode.isGog() ? this.ALERT_CATEGORY.LOG_ANALYTICS_ALERT : this.getCategoryByLevel(treeNode.getActualTreeNodeLevel());
      treeNode.addToExtraData('category', category);

      // Remove data from extra data of read only alerts
      if (category == this.ALERT_CATEGORY.LOG_ANALYTICS_READ_ONLY) {
          treeNode.removeFromExtraData(['meaningful_entities', 'correlators']);
      }
  },

  getCategoryByLevel: function(treeLevel) {
      treeLevel = parseInt(treeLevel, 10);
      switch (treeLevel) {
          case 1:
              return this.ALERT_CATEGORY.LOG_ANALYTICS_GROUP;
          case 2:
              return this.ALERT_CATEGORY.LOG_ANALYTICS_ALERT;
          default:
              return this.ALERT_CATEGORY.LOG_ANALYTICS_READ_ONLY;
      }
  },

  // construct identified issue hint according to use case
  getIdentifiedIssueHint: function(alertData) {
      var result = "";
      if (alertData.detectionType == "SIGNAL_DEAD") {
          result = gs.getMessage("No data is streaming");
      } else {
          switch (alertData.metric.type) {
              case "PatternIdMetric":
                  if (alertData.detectionType == "NEW_SIGNAL") {
                      var component = alertData.service;
                      result = gs.getMessage("This is the first time this pattern appears in '{0}'", component);
                  } else {
                      result = gs.getMessage("The following pattern is anomalous");
                  }
                  break;

              case "RawMetric":
                  result = gs.getMessage("The following metric is anomalous");
                  break;

              case "CustomMetric":
                  result = gs.getMessage("A manually defined threshold has been crossed");
                  break;

              case "AllEventsMetric":
                  result = gs.getMessage("There is a spike in the total volume of events");
                  break;

              case "KeywordMetric":
                  var keyword = alertData.metric.properties.keyword;
                  if (this.hasHolmesProperty(alertData)) {
                      result = gs.getMessage("This is the dominant contributing pattern for the anomaly. The alert was triggered by a surge in the keyword '{0}'", keyword);
                  } else {
                      result = gs.getMessage("The alert was triggered by a surge in the keyword '{0}'", keyword);
                  }
                  break;

              case "PatternSeverityMetric":
                  var severity = alertData.metric.properties.severity;
                  if (this.hasHolmesProperty(alertData)) {
                      result = gs.getMessage("This is the dominant contributing pattern for the anomaly. The alert was triggered by a surge in the log severity level '{0}'", severity);
                  } else {
                      result = gs.getMessage("The alert was triggered by a surge in the log severity level '{0}'", severity);
                  }
                  break;

              case "HistogramMetric":
                  result = gs.getMessage("The relative propotion of the following value is anomalous");
                  break;

              default:
                  result = gs.getMessage("The following metric is anomalous");
          }
      }
      return result;
  },

  getCorrelators: function(alertData) {
      var result = "";
      if (alertData.hasOwnProperty("correlatingTags") && alertData.correlatingTags) {
          var data = alertData.correlatingTags;
          for (var h = 0; h < data.length; h++) {
              var key = Object.keys(data[h])[0];
              var value = data[h][key];
              key = key.replace("__loom_pattern_text", "Log Pattern");
              key = key.replace("__loom_detection_time", "Occurrence Time");
              key = key.replace("__loom_detection_type", "Anomaly Type");
              key = key.replace("__loom_detection_trend_up", "Anomaly Trend Up");
              key = key.replace("__loom_detection_trend_down", "Anomaly Trend Down");
              key = key.replace("__loom_application", "Application service");
              key = key.replace("__loom_service", "Component");
              key = key.replace("__loom_host", "Assigned Host");
              if (key == "__loom_in_text") // this is deprecated
                  continue;

              if (key == "Log Pattern") {
                  value = "Correlation based on similar text patterns '" + value.slice(0, 20) + "...'";
              }

              var pair = key + ": " + value;

              // for top level alert level
              result += pair + ",";

              // for incident level
              if (!this.correlatorsMap.hasOwnProperty(pair)) {
                  this.correlatorsMap[pair] = 1;
              } else {
                  this.correlatorsMap[pair]++;
              }
          }

          return result;
      }
  },

  removeSingleAlertCorrelators: function() {
      var updatedCorrelatorsMap = {};
      var map = this.correlatorsMap;
      Object.keys(this.correlatorsMap).forEach(function(key) {
          if (map[key] > 1) {
              updatedCorrelatorsMap[key] = map[key];
          }
      });

      return updatedCorrelatorsMap;
  },

  percentageAbsoluteVal: function(changePercentage) {

      if (!changePercentage)
          return "";

      var changePercentageVal = Number(changePercentage);


      if (changePercentageVal < 0)
          changePercentageVal *= -1;

      // if there is only 0 after the point we want to present a whole number
      if (changePercentageVal - parseInt(changePercentageVal) == 0)
          return changePercentageVal.toFixed();

      return changePercentageVal;
  },

  formatTwo: function(val) {

      if (!val)
          return "";
      var x = parseInt(Math.round(val * 100), 10);
      return (x / 100).toString();
  },

  // find patternText from decomposed frequent patterns 
  getDecomposedPatternsText: function(alertData, patternTextArr, kbSearchDelimiter, forShortDescription) {

      if (this.isValidTag(alertData, "decomposedFrequentPatterns") && alertData.decomposedFrequentPatterns.length > 0) {
          var dfp = alertData.decomposedFrequentPatterns;
          for (var idx = 0; idx < dfp.length; idx++) {
              if (this.isValidTag(dfp[idx], "decomposedPattern")) {
                  var dp = dfp[idx].decomposedPattern;
                  if (this.isValidTag(dp, "pattern")) {
                      var pattern = dp.pattern;
                      if (this.isValidTag(pattern, "patternText")) {
                          var patternTextWithQuotes = "\"" + pattern.patternText + "\"";
                          if (patternTextArr.indexOf(patternTextWithQuotes) < 0) {
                              this.addDelimiter(kbSearchDelimiter, patternTextArr, forShortDescription);
                              patternTextArr.push(patternTextWithQuotes);
                          }
                      }
                      if (this.isValidTag(pattern, "patternExternalId")) {
                          if (patternTextArr.indexOf(pattern.patternExternalId) < 0) {
                              this.addDelimiter(kbSearchDelimiter, patternTextArr, forShortDescription);
                              patternTextArr.push(pattern.patternExternalId);
                          }
                      }
                  }
              }
          }
      }
  },

  getKBSearchInfo: function(alertData, patternText, patternExternalId, forShortDescription, description) {

      //Check in the JSON for pattern alerts, search for "patternExternalId" property. 
      //This property should always work 
      //assuming the user labeled the external ID label in the source type structure
      //If it's not a pattern alert we should search for "event_id". 
      //We should make sure that we also catch all the combination 
      //(phases) of event_id ,i.e: winlog.event_id
      //pattern text matching - if we didn't find any matching based on reference ID 
      //we should search the pattern text property value ("patternText") 
      //also directly under the alertData and inside the decomposedFrequentPatterns
      var resultArr = [];
      var result = "";

      var kbSearchDelimiter = gs.getProperty('evt_mgmt.log_analytics.kb_search_demiliter', "OR"); // set this property to false to disable adding delimiter
      if (kbSearchDelimiter && kbSearchDelimiter != "false")
          kbSearchDelimiter = " " + kbSearchDelimiter + " ";
      else
          kbSearchDelimiter = "";

      try {
          if (alertData.properties) {
              if (this.isValidTag(alertData.properties, "ExternalId")) {
                  resultArr.push(alertData.properties.ExternalId);
              }
              if (patternExternalId) {
                  if (resultArr.indexOf(patternExternalId) < 0) {
                      this.addDelimiter(kbSearchDelimiter, resultArr, forShortDescription);
                      resultArr.push(patternExternalId);
                  }
              }
              //event id
              for (var k in alertData.properties) {
                  if (k.includes("event_id")) {
                      if (resultArr.indexOf(alertData.properties[k]) < 0) {
                          this.addDelimiter(kbSearchDelimiter, resultArr, forShortDescription);
                          resultArr.push(alertData.properties[k]);
                      }
                  }
              }
              if (patternText) {
                  var patternTextWithQuotes = "\"" + patternText + "\"";
                  if (resultArr.indexOf(patternTextWithQuotes) < 0) {
                      this.addDelimiter(kbSearchDelimiter, resultArr, forShortDescription);
                      resultArr.push(patternTextWithQuotes);
                  }
              }
              if (forShortDescription) {
                  //expand data from decomposed frequent patterns 
                  this.getDecomposedPatternsText(alertData, resultArr, kbSearchDelimiter, forShortDescription);
              }

              result = resultArr.join(" ");
              if (!result)
                  result = "\"" + description + "\""; // We didnt found any field that helps us with KB search so we set the description
          }
      } catch (err) {
          gs.error("EvtMgmtProcessLogAnalyticsJson.getKBSearchInfo failed: " + err);
      }
      return result;
  },

  addDelimiter: function(delimiter, resultArr, forShortDescription) {
      if (forShortDescription && delimiter && resultArr.length > 0) {
          resultArr.push(delimiter);
      }
  },

  hasHolmesProperty: function(alertData) {
      return (alertData.hasOwnProperty("holmes") && alertData.holmes && alertData.holmes.length > 0);
  },

  getAnomalyTrendDescription: function(detectionType) {
      var trendDescription = gs.getMessage("anomalous");
      if (detectionType) {
          if (detectionType.indexOf('_INCREASE') > 0 || detectionType.indexOf('_ABOVE') > 0 || detectionType.indexOf('_ALIVE') > 0) {
              trendDescription = gs.getMessage("above normal");
          } else if (detectionType.indexOf('_DECREASE') > 0 || detectionType.indexOf('_BELOW') > 0 || detectionType.indexOf('_DEAD') > 0) {
              trendDescription = gs.getMessage("below normal");
          }
      }
      return trendDescription;
  },
};

Sys ID

de00a514b74310107c038229ce11a923

Offical Documentation

Official Docs: