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.
212 lines
8.5 KiB
212 lines
8.5 KiB
# Copyright (C) 2005-2025 Splunk Inc. All Rights Reserved.
|
|
import collections
|
|
|
|
import logging
|
|
import re
|
|
import csv
|
|
import sys
|
|
|
|
from splunk.clilib.bundle_paths import make_splunkhome_path
|
|
|
|
sys.path.append(make_splunkhome_path(['etc', 'apps', 'SA-ITOA', 'lib']))
|
|
import itsi_path
|
|
from ITOA.setup_logging import setup_logging
|
|
from itsi.itsi_time_block_utils import PolicyFilter
|
|
from ITOA.itoa_common import is_feature_enabled
|
|
from at_utils.chunked_util import read_chunk, write_chunk, add_message
|
|
from at_utils.utils import log_and_die, log_and_warn, gather_input_data, detect_outliers, MIN_DATASET_LEN
|
|
|
|
##################
|
|
# itsi_outlier
|
|
##################
|
|
# Command logs to $SPLUNK_HOME/var/log/splunk/itsi_outlier.log
|
|
|
|
|
|
# Windows will mangle our line-endings unless we do this.
|
|
if sys.platform == "win32":
|
|
import os
|
|
import msvcrt
|
|
|
|
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
|
|
msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
|
|
msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
|
|
|
|
logger = setup_logging("itsi_outlier_command.log", "itsi.apply_at.outlier", level=logging.DEBUG)
|
|
|
|
|
|
def parse_args(args, in_metadata, out_metadata, logger):
|
|
params = {}
|
|
params['use_kv_store'] = True
|
|
params['entity_level_thresholds'] = False
|
|
params['threshold_key'] = 'aggregate_thresholds'
|
|
|
|
if 'nokv' in args:
|
|
params['use_kv_store'] = False
|
|
|
|
if 'remove' in args:
|
|
# Detects and remove outliers, default behavior is to
|
|
# display is_outlier field set to true per policy block
|
|
params['remove'] = True
|
|
else:
|
|
params['remove'] = False
|
|
|
|
if 'show_outliers_only' in args:
|
|
params['show_outliers_only'] = True
|
|
else:
|
|
params['show_outliers_only'] = False
|
|
|
|
r = re.search(r'\s*method\s*=\s*(?P<method>\S+)\'', str(args))
|
|
if r is not None:
|
|
try:
|
|
params['method'] = r.group('method')
|
|
logger.debug(
|
|
"Outlier detection method passed: %s" % str(params['method']))
|
|
except Exception:
|
|
log_and_die(metadata=out_metadata, logger=logger,
|
|
msg='Failed to parse outlier detection method in parameters.')
|
|
else:
|
|
log_and_die(metadata=out_metadata, logger=logger,
|
|
msg='Must provide a outlier detection method.')
|
|
|
|
r = re.search(r'\s*sensitivity\s*=\s*(?P<sensitivity>\S+)\'', str(args))
|
|
if r is not None:
|
|
try:
|
|
params['multiplier'] = r.group('sensitivity')
|
|
logger.debug(
|
|
"Outlier detection multiplier passed: %s" %
|
|
str(params['multiplier'])
|
|
)
|
|
except Exception:
|
|
log_and_die(
|
|
metadata=out_metadata, logger=logger,
|
|
msg='Failed to parse outlier detection multiplier in parameters.')
|
|
else:
|
|
log_and_die(metadata=out_metadata, logger=logger,
|
|
msg='Must provide a outlier detection multiplier.')
|
|
|
|
params['session_key'] = str(in_metadata['searchinfo']['session_key'])
|
|
return params
|
|
|
|
|
|
def main():
|
|
logger.debug("Starting ITSI Outlier detection.")
|
|
|
|
out_metadata = {}
|
|
out_metadata['inspector'] = {'messages': []}
|
|
|
|
# Phase 0: getinfo exchange
|
|
metadata, body = read_chunk(sys.stdin, logger)
|
|
# Don't run in preview.
|
|
if metadata.get('preview', False):
|
|
write_chunk(sys.stdout, {'finished': True}, '')
|
|
sys.exit(0)
|
|
|
|
args = str(metadata['searchinfo']['args'])
|
|
|
|
params = parse_args(
|
|
args=args, in_metadata=metadata, out_metadata=out_metadata, logger=logger)
|
|
params['logger'] = logger # Using logger from the module to be passed along to utils.
|
|
params['out_metadata'] = out_metadata
|
|
|
|
if not is_feature_enabled('itsi-at-outlier-removal', params['session_key']):
|
|
logger.error("itsioutlier command is not enabled.")
|
|
sys.exit(1)
|
|
|
|
params['out_metadata']['finished'] = False
|
|
fields_list = ['policy_key', 'itsi_service_id', 'itsi_kpi_id', 'alert_value', '_time']
|
|
params['out_metadata']['required_fields'] = fields_list
|
|
params['out_metadata']['type'] = 'reporting'
|
|
write_chunk(sys.stdout, params['out_metadata'], '')
|
|
params['out_metadata'].pop('type', None)
|
|
params['out_metadata'].pop('required_fields', None)
|
|
gather_input_data(params, logger, fields_list)
|
|
kpidict = params['kpidict']
|
|
|
|
for itsi_service_id in kpidict:
|
|
params['kpi'] = {
|
|
'service_id': itsi_service_id,
|
|
'service_data': {}
|
|
}
|
|
# kpidict: {'service-id': {'kpi-id': {'_time': [0....N], 'alert_value': [0....N], 'itsi_kpi_id': [0....N],
|
|
# 'itsi_service_id':[0...N], 'policy_key': [0.....N]}}}
|
|
for itsi_kpi_id in kpidict[itsi_service_id]:
|
|
params['kpi']['kpi_id'] = itsi_kpi_id
|
|
if not read_chunk(sys.stdin, logger):
|
|
break
|
|
|
|
data = kpidict[itsi_service_id][itsi_kpi_id]
|
|
# data is a dictionary of field - list pairs
|
|
policy_keys = data['policy_key']
|
|
alert_values = data['alert_value']
|
|
time_stamps = data['_time']
|
|
# Create policy_chunks as array of tuples:
|
|
# example: [('178.64064691379593', '1664820000.0', False), ('179.75162587638422', '1664820060.0', False),...]
|
|
policy_chunks = collections.OrderedDict()
|
|
# Build policy chunks for outlier detection
|
|
for x in range(len(policy_keys)):
|
|
key = policy_keys[x]
|
|
# Create a tuple entry as (alert_value, _time, is_outler, upper_bound, lower_bound)
|
|
entry = (alert_values[x], time_stamps[x], False, 0, 0)
|
|
if key in policy_chunks:
|
|
policy_chunks[key].append(entry)
|
|
else:
|
|
policy_chunks[key] = []
|
|
policy_chunks[key].append(entry)
|
|
|
|
# Perform the min dataset check
|
|
valid_policy_chunks = {}
|
|
for policy in policy_chunks.keys():
|
|
if len(policy_chunks[policy]) < MIN_DATASET_LEN:
|
|
log_and_warn(
|
|
metadata=out_metadata, logger=logger,
|
|
msg='Not enough data to detect outliers for policy block %s of %s, %s ' %
|
|
(key, itsi_service_id, itsi_kpi_id))
|
|
else:
|
|
valid_policy_chunks[policy] = policy_chunks[policy]
|
|
# Detect outliers only on valid policy chunks
|
|
policy_outlier_map = detect_outliers(params, valid_policy_chunks)
|
|
|
|
# prepare for generating output
|
|
params['out_metadata']['finished'] = False
|
|
fields_list = ['policy_key']
|
|
fields_list = fields_list + ['itsi_service_id', 'itsi_kpi_id', 'alert_value', '_time', 'is_outlier', 'lower_bound', 'upper_bound']
|
|
params['kpi']['writer'] = csv.DictWriter(
|
|
params['outbuf'], fieldnames=fields_list, dialect='excel', extrasaction='ignore')
|
|
params['kpi']['writer'].writeheader()
|
|
# write output to buffer
|
|
# If show outliers only is set, policy_outlier_map has outliers for valid chunks
|
|
# else, policy chunks has both valid and invalid chunks, with invalid chunks default is_outlier set to False.
|
|
data = policy_outlier_map if params['show_outliers_only'] else policy_chunks
|
|
lines = []
|
|
for policy_key, val_tuples in data.items():
|
|
if policy_key in valid_policy_chunks:
|
|
# Use val_tuples from valid policy chunks if available
|
|
# else use val_tuples from policy_chunks
|
|
val_tuples = valid_policy_chunks[policy_key]
|
|
for entry in val_tuples:
|
|
if params['remove'] and entry[2]:
|
|
# Remove outliers from the output
|
|
continue
|
|
line = {
|
|
'policy_key': policy_key,
|
|
'itsi_service_id': params['kpi']['service_id'], 'itsi_kpi_id': params['kpi']['kpi_id'],
|
|
'alert_value': entry[0], '_time': entry[1], 'is_outlier': entry[2], 'lower_bound': entry[3], 'upper_bound': entry[4]
|
|
}
|
|
lines.append(line)
|
|
# output the results
|
|
time_sorted_lines = sorted(lines, key=lambda ll: ll['_time'])
|
|
params['kpi']['writer'].writerows(time_sorted_lines)
|
|
write_chunk(
|
|
sys.stdout, params['out_metadata'], params['outbuf'].getvalue())
|
|
|
|
ret = read_chunk(sys.stdin, logger)
|
|
if ret:
|
|
write_chunk(sys.stdout, {"finished": True}, '')
|
|
|
|
logger.debug(
|
|
"Finished ITSI outlier detection.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|