You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
304 lines
16 KiB
304 lines
16 KiB
# Copyright (C) 2005-2024 Splunk Inc. All Rights Reserved.
|
|
|
|
from .itsi_atad_search_base import ItsiAtAdSearchBase
|
|
from itsi.objects.itsi_kpi_entity_threshold import ItsiKpiEntityThreshold
|
|
from itsi.objects.itsi_kpi_at_info import ItsiKpiAtInfo
|
|
from itsi.objects.itsi_service import ItsiService
|
|
from ITOA.setup_logging import logger
|
|
from string import Template
|
|
from ITOA.itoa_common import is_feature_enabled
|
|
|
|
AT_SEARCH_TEMPLATE = Template(" | mstats latest(alert_value) AS alert_value latest(alert_level) AS alert_level WHERE "
|
|
" `get_itsi_summary_metrics_index` AND ($kpi_filter_string) AND is_filled_gap_event!=1 "
|
|
" AND is_null_alert_value=0 `$metrics_macro_filter` by $filter_fields "
|
|
" span=1m | where alert_level!=-2 | table _time, alert_value, "
|
|
" alert_level, $filter_fields | $command ")
|
|
|
|
HIGH_SCALE_AT_SEARCH_TEMPLATE = Template("| inputlookup kpi_at_info_lookup "
|
|
"| search adaptive_thresholding_training_window=${training_days} "
|
|
"| sort limit=0 service_id "
|
|
"| rename _key as kpi_id "
|
|
"| fields kpi_id "
|
|
"| itsibatchat training_window=${training_days}")
|
|
|
|
HIGH_SCALE_ENTITY_AT_SEARCH_TEMPLATE = Template("| inputlookup entity_thresholds_at_lookup "
|
|
"| search adaptive_thresholds_is_enabled=1 adaptive_thresholding_training_window=${training_days} "
|
|
"| sort limit=0 kpi_id "
|
|
"| fields kpi_id, entity_key, entity_title "
|
|
"| itsibatchat entitylevelthreshold=True training_window=${training_days}")
|
|
|
|
|
|
class ItsiAtSearch(ItsiAtAdSearchBase):
|
|
"""
|
|
Implements ITSI AT Search
|
|
Contains CRUD operations and utility functions pertaining to AT searches
|
|
"""
|
|
|
|
log_prefix = '[ITSI AT Search] '
|
|
collection_name = 'itsi_service'
|
|
|
|
def __init__(self, session_key):
|
|
super(ItsiAtSearch, self).__init__(session_key, logger=logger)
|
|
|
|
def make_search_name(self, training_window_id):
|
|
"""
|
|
Generate name for AT search stanza
|
|
@param training_window_id: training window string
|
|
@returns: name for AT search stanza
|
|
"""
|
|
return 'itsi_at_' + self._make_search_name_suffix(training_window_id)
|
|
|
|
def make_high_scale_at_search_name(self, training_window_id, object_type="kpi"):
|
|
"""
|
|
Generate name for AT search stanza
|
|
@param training_window_id: training window string
|
|
@param object_type: object_type to make saved search name. Valid values: kpi, entity
|
|
@returns: name for AT search stanza
|
|
"""
|
|
return 'itsi_batch_at_' + self._make_search_name_suffix(training_window_id, object_type=object_type)
|
|
|
|
def get_all_searches(self):
|
|
"""
|
|
@returns: dict of saved searches keyed by stanza name
|
|
"""
|
|
return self._get_all_searches(lambda x: x.startswith('itsi_at_search'))
|
|
|
|
def get_high_scale_at_searches(self, object_type="kpi"):
|
|
"""
|
|
@returns: dict of high scale AT saved searches keyed by stanza name
|
|
"""
|
|
return self._get_all_searches(lambda x: x.startswith('itsi_batch_at_search_{0}'.format(object_type)))
|
|
|
|
def make_search(self, **params):
|
|
"""
|
|
Generate AT search string
|
|
@param **params: keyword args needed for search generation;
|
|
required args: `kpi_filter_string`
|
|
@returns: AT search string
|
|
"""
|
|
if 'kpi_filter_string' not in params:
|
|
logger.error("Missing parameters to search template, got %s", params)
|
|
raise Exception("Missing parameters to search template")
|
|
if 'command' not in params:
|
|
params["command"] = "applyat" if is_feature_enabled('itsi-at-outlier-removal', self.session_key) else 'itsiat'
|
|
if 'metrics_macro_filter' not in params:
|
|
params["metrics_macro_filter"] = "metrics_service_level_kpi_only"
|
|
if 'filter_fields' not in params:
|
|
params["filter_fields"] = "itsi_kpi_id, itsi_service_id"
|
|
return AT_SEARCH_TEMPLATE.substitute(**params)
|
|
|
|
def make_high_scale_at_search(self, **params):
|
|
"""
|
|
Generate search string that calls the batching version of the AT search
|
|
@param **params: keyword args needed for search generation;
|
|
required args: `kpi_filter_string`
|
|
@returns: AT search string
|
|
"""
|
|
if params.get("object_type", "kpi") == "entity":
|
|
return HIGH_SCALE_ENTITY_AT_SEARCH_TEMPLATE.substitute(**params)
|
|
return HIGH_SCALE_AT_SEARCH_TEMPLATE.substitute(**params)
|
|
|
|
def compute_earliest_time(self, training_window):
|
|
"""
|
|
Compute earliest time (in relative minutes) for AT search
|
|
@param training_window: AT training window, as a relative time spec in days, minutes, or hours ('-Xd' or '-Xm')
|
|
@returns: relative time string for search earliest time in minutes, e.g. '-1940m'
|
|
"""
|
|
return '-%sm' % (self.to_minutes(training_window))
|
|
|
|
def make_saved_search_params(self, name, search, et, kpi_list):
|
|
"""
|
|
@param name: search stanza name
|
|
@param search: search string
|
|
@param et: earliest time (relative timespec)
|
|
@param kpi_list: list of KPI IDs
|
|
@returns: dict of key/value pairs for the saved search stanza
|
|
"""
|
|
return {
|
|
'name': name,
|
|
'disabled': False,
|
|
'enableSched': 1,
|
|
'cron_schedule': '0 0 * * *',
|
|
'dispatch.earliest_time': et,
|
|
'dispatch.latest_time': 'now',
|
|
'action.summary_index._kpi_id_list': ','.join(kpi_list),
|
|
'action.summary_index._et': et, # save earliest time to persist it in format user can't override
|
|
'search': search
|
|
}
|
|
|
|
def get_num_training_days(self, search, search_name):
|
|
"""
|
|
@param search: existing at search
|
|
@param search_name: name of the existing at search
|
|
@returns: int of number of training days
|
|
"""
|
|
try:
|
|
_et = search['content'].get('action.summary_index._et')
|
|
except KeyError:
|
|
_et = search['content'].get('dispatch.earliest_time')
|
|
if not _et:
|
|
# ITSI-31527: For scenarios where _et is not set and assuming
|
|
# saved searches are named as 'itsi_at_search_kpi_minusXY'
|
|
# where X is number of, Y - hours(h), minutes(m), days(d)
|
|
suffix = search_name.split("itsi_at_search_kpi_minus")[1]
|
|
_et = "-" + suffix
|
|
return self.to_days(_et)
|
|
|
|
def rewrite_saved_searches(self):
|
|
"""
|
|
@returns: Returns a list of tuples of failed search name and exception if any
|
|
"""
|
|
is_high_scale_at_enabled = is_feature_enabled('itsi-high-scale-at', self.session_key)
|
|
is_entity_level_at_enabled = is_feature_enabled('itsi-entity-level-adaptive-thresholding', self.session_key)
|
|
|
|
existing_at_searches = self.get_all_searches()
|
|
failed_rewrite_searches = []
|
|
kpi_at_info_interface = ItsiKpiAtInfo(self.session_key, 'nobody')
|
|
entity_thresholds_interface = ItsiKpiEntityThreshold(self.session_key, 'nobody')
|
|
# finds all kpis with AT enabled
|
|
service_interface = ItsiService(self.session_key, 'nobody')
|
|
all_at_kpis = []
|
|
entity_thresholding_enabled_services = []
|
|
|
|
at_enabled_kpi_services = service_interface.get_bulk(
|
|
'nobody',
|
|
filter_data={"kpis.adaptive_thresholds_is_enabled": True},
|
|
fields=['kpis.service_id', 'kpis._key', 'kpis.adaptive_thresholding_training_window', 'kpis.adaptive_thresholds_is_enabled']
|
|
)
|
|
|
|
for service in at_enabled_kpi_services:
|
|
for kpi in service["kpis"]:
|
|
if kpi["adaptive_thresholds_is_enabled"]:
|
|
all_at_kpis.append(kpi)
|
|
|
|
# Only get objects if entity level AT or High Scale AT not enabled
|
|
if not is_entity_level_at_enabled or not is_high_scale_at_enabled:
|
|
entity_thresholding_enabled_services = service_interface.get_bulk('nobody', filter_data={"kpis.is_entity_level_thresholding": True})
|
|
|
|
for service in entity_thresholding_enabled_services:
|
|
for kpi in service["kpis"]:
|
|
kpi["is_entity_level_thresholding"] = False
|
|
|
|
supported_kpi_training_windows = ["-7d", "-14d", "-30d", "-60d"]
|
|
supported_entity_training_windows = ["-7d", "-14d"]
|
|
|
|
if not is_high_scale_at_enabled:
|
|
kpis_by_training_window = {}
|
|
|
|
for kpi in all_at_kpis:
|
|
if kpi['adaptive_thresholding_training_window'] not in kpis_by_training_window.keys():
|
|
kpis_by_training_window[kpi['adaptive_thresholding_training_window']] = [kpi['_key']]
|
|
else:
|
|
kpis_by_training_window[kpi['adaptive_thresholding_training_window']].append(kpi['_key'])
|
|
|
|
# remove itsibatchat searches
|
|
high_scale_searches = self.get_high_scale_at_searches("")
|
|
for high_scale_search_name, high_scale_search in high_scale_searches.items():
|
|
try:
|
|
self.delete_saved_search(high_scale_search_name)
|
|
except Exception as e:
|
|
self.logger.exception("Exception while deleting saved search for itsi-high-scale-at: %s, %s" % (high_scale_search_name, str(e)))
|
|
failed_rewrite_searches.append((high_scale_search_name, e))
|
|
|
|
# delete at_info records, entity thresholds and disable entity_thresholding on KPIs
|
|
kpi_at_info_interface.delete_bulk('nobody')
|
|
entity_thresholds_interface.delete_bulk('nobody')
|
|
if entity_thresholding_enabled_services:
|
|
service_interface.save_batch("nobody", entity_thresholding_enabled_services, False, req_source="rewrite_saved_searches")
|
|
|
|
# create applyat searches if they do not exist
|
|
for training_window in kpis_by_training_window.keys():
|
|
search_name = self.make_search_name(training_window)
|
|
kpis = kpis_by_training_window[training_window]
|
|
kpi_filter_string = " OR ".join("itsi_kpi_id=\"" + x + "\"" for x in kpis)
|
|
search_string = self.make_search(kpi_filter_string=kpi_filter_string, training_days=training_window)
|
|
et = self.compute_earliest_time(training_window)
|
|
try:
|
|
if search_name not in existing_at_searches.keys():
|
|
self.create_saved_search(search_name, search_string, et, kpis)
|
|
else:
|
|
self.update_saved_search(search_name, search_string, et, kpis)
|
|
|
|
except Exception as e:
|
|
self.logger.exception("Exception while rewriting saved search after disabling itsi-high-scale-at: %s, %s" % (search_name, str(e)))
|
|
failed_rewrite_searches.append((search_name, e))
|
|
|
|
else:
|
|
# removes old applyat searches
|
|
for existing_search_name, existing_search in existing_at_searches.items():
|
|
try:
|
|
self.delete_saved_search(existing_search_name)
|
|
except Exception as e:
|
|
self.logger.exception("Exception while deleting saved search for itsi-high-scale-at: %s, %s" % (existing_search_name, str(e)))
|
|
failed_rewrite_searches.append((existing_search_name, e))
|
|
|
|
# gets existing kpi_entity_objects
|
|
at_info_records = kpi_at_info_interface.get_bulk('nobody')
|
|
at_info_records_by_kpi = {}
|
|
for record in at_info_records:
|
|
at_info_records_by_kpi[record['_key']] = record
|
|
new_at_info_records = []
|
|
|
|
for kpi in all_at_kpis:
|
|
# creates new at_info record
|
|
if kpi['_key'] not in at_info_records_by_kpi.keys():
|
|
new_at_info_records.append({
|
|
'service_id': kpi['service_id'],
|
|
'adaptive_thresholding_training_window': kpi['adaptive_thresholding_training_window'],
|
|
'object_type': 'kpi_at_info',
|
|
'_key': kpi['_key'],
|
|
})
|
|
# updates existing at_info record only if training window is different
|
|
else:
|
|
record = at_info_records_by_kpi.pop(kpi['_key'])
|
|
if record['adaptive_thresholding_training_window'] != kpi['adaptive_thresholding_training_window']:
|
|
record['adaptive_thresholding_training_window'] = kpi['adaptive_thresholding_training_window']
|
|
new_at_info_records.append(record)
|
|
|
|
# upserts new and updated at_info records
|
|
if len(new_at_info_records):
|
|
kpi_at_info_interface.save_batch('nobody', new_at_info_records, False, req_source='rewrite_saved_searches')
|
|
|
|
# creates itsibatchat searches for KPIs if they don't exist
|
|
high_scale_kpi_searches = self.get_high_scale_at_searches()
|
|
for training_window in supported_kpi_training_windows:
|
|
high_scale_search_name = self.make_high_scale_at_search_name(training_window)
|
|
search_string = self.make_high_scale_at_search(training_days=training_window)
|
|
et = self.compute_earliest_time(training_window)
|
|
try:
|
|
if high_scale_search_name not in high_scale_kpi_searches.keys():
|
|
self.create_saved_search(high_scale_search_name, search_string, et)
|
|
except Exception as e:
|
|
self.logger.exception("Exception while rewriting saved search: %s, %s" % (high_scale_search_name, str(e)))
|
|
failed_rewrite_searches.append((high_scale_search_name, None))
|
|
|
|
high_scale_entity_searches = self.get_high_scale_at_searches("entity")
|
|
if is_entity_level_at_enabled:
|
|
# creates itsibatchat searches for Entities if they don't exist
|
|
for training_window in supported_entity_training_windows:
|
|
high_scale_search_name = self.make_high_scale_at_search_name(training_window, "entity")
|
|
search_string = self.make_high_scale_at_search(training_days=training_window, object_type="entity")
|
|
et = self.compute_earliest_time(training_window)
|
|
try:
|
|
if high_scale_search_name not in high_scale_entity_searches.keys():
|
|
self.create_saved_search(high_scale_search_name, search_string, et)
|
|
except Exception as e:
|
|
self.logger.exception("Exception while rewriting saved search: %s, %s" % (high_scale_search_name, str(e)))
|
|
failed_rewrite_searches.append((high_scale_search_name, None))
|
|
else:
|
|
# Remove All Entity Thresholds on disabling the feature flag
|
|
# Disable is_entity_level_thresholding value in all KPIs
|
|
entity_thresholds_interface.delete_bulk('nobody')
|
|
if entity_thresholding_enabled_services:
|
|
service_interface.save_batch("nobody", entity_thresholding_enabled_services, False, req_source="rewrite_saved_searches")
|
|
|
|
# Delete itsibatchat searches for Entities if feature flag is disabled
|
|
for high_scale_search_name, high_scale_search in high_scale_entity_searches.items():
|
|
try:
|
|
self.delete_saved_search(high_scale_search_name)
|
|
except Exception as e:
|
|
self.logger.exception("Exception while deleting saved search for itsi-entity-level-adaptive-thresholding: %s, %s" % (high_scale_search_name, str(e)))
|
|
failed_rewrite_searches.append((high_scale_search_name, e))
|
|
|
|
return failed_rewrite_searches
|