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.
438 lines
21 KiB
438 lines
21 KiB
#!/usr/bin/env python
|
|
# coding=utf-8
|
|
|
|
__author__ = "TrackMe Limited"
|
|
__copyright__ = "Copyright 2022-2026, TrackMe Limited, U.K."
|
|
__credits__ = "TrackMe Limited, U.K."
|
|
__license__ = "TrackMe Limited, all rights reserved"
|
|
__version__ = "0.1.0"
|
|
__maintainer__ = "TrackMe Limited, U.K."
|
|
__email__ = "support@trackme-solutions.com"
|
|
__status__ = "PRODUCTION"
|
|
|
|
import os
|
|
import sys
|
|
import json
|
|
from collections import OrderedDict
|
|
import time
|
|
import logging
|
|
from logging.handlers import RotatingFileHandler
|
|
from urllib.parse import urlencode
|
|
import urllib.parse
|
|
import urllib3
|
|
|
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
|
|
# splunk home
|
|
splunkhome = os.environ["SPLUNK_HOME"]
|
|
|
|
# append lib
|
|
sys.path.append(os.path.join(splunkhome, "etc", "apps", "trackme", "lib"))
|
|
|
|
# import TrackMe libs
|
|
from trackme_libs import JSONFormatter
|
|
|
|
# import trackme libs utils
|
|
from trackme_libs_utils import (
|
|
escape_backslash,
|
|
replace_encoded_doublebackslashes,
|
|
remove_leading_spaces,
|
|
)
|
|
|
|
# logging:
|
|
# To avoid overriding logging destination of callers, the libs will not set on purpose any logging definition
|
|
# and rely on callers themselves
|
|
|
|
def trackme_fqm_gen_metrics(
|
|
timestamp, tenant_id, object_value, object_id, metric_index, metrics_event
|
|
):
|
|
try:
|
|
if not isinstance(metrics_event, dict):
|
|
metrics_event = json.loads(metrics_event)
|
|
|
|
# Create a dedicated logger for FLX metrics
|
|
fqm_logger = logging.getLogger("trackme.fqm.metrics")
|
|
fqm_logger.setLevel(logging.INFO)
|
|
|
|
# Only add the handler if it doesn't exist yet
|
|
if not fqm_logger.handlers:
|
|
# Set up the file handler
|
|
filehandler = RotatingFileHandler(
|
|
f"{splunkhome}/var/log/splunk/trackme_fqm_metrics.log",
|
|
mode="a",
|
|
maxBytes=100000000,
|
|
backupCount=1,
|
|
)
|
|
formatter = JSONFormatter(timestamp=timestamp)
|
|
filehandler.setFormatter(formatter)
|
|
fqm_logger.addHandler(filehandler)
|
|
# Prevent propagation to root logger
|
|
fqm_logger.propagate = False
|
|
else:
|
|
# Find the RotatingFileHandler among existing handlers
|
|
filehandler = None
|
|
for handler in fqm_logger.handlers:
|
|
if isinstance(handler, RotatingFileHandler):
|
|
filehandler = handler
|
|
break
|
|
|
|
# If no RotatingFileHandler found, create one
|
|
if filehandler is None:
|
|
filehandler = RotatingFileHandler(
|
|
f"{splunkhome}/var/log/splunk/trackme_fqm_metrics.log",
|
|
mode="a",
|
|
maxBytes=100000000,
|
|
backupCount=1,
|
|
)
|
|
fqm_logger.addHandler(filehandler)
|
|
|
|
# Update formatter with current timestamp
|
|
formatter = JSONFormatter(timestamp=timestamp)
|
|
filehandler.setFormatter(formatter)
|
|
|
|
fqm_logger.info(
|
|
"Metrics - group=fqm_metrics",
|
|
extra={
|
|
"target_index": metric_index,
|
|
"tenant_id": tenant_id,
|
|
"object": object_value,
|
|
"object_id": object_id,
|
|
"object_category": "splk-fqm",
|
|
"metrics_event": json.dumps(metrics_event),
|
|
},
|
|
)
|
|
|
|
except Exception as e:
|
|
raise Exception(str(e))
|
|
|
|
|
|
def trackme_fqm_gen_metrics_from_list(
|
|
tenant_id, metric_index, metrics_list
|
|
):
|
|
try:
|
|
if not isinstance(metrics_list, list):
|
|
metrics_list = json.loads(metrics_list)
|
|
|
|
# Create a dedicated logger for FQM metrics
|
|
fqm_logger = logging.getLogger("trackme.fqm.metrics")
|
|
fqm_logger.setLevel(logging.INFO)
|
|
|
|
# Only add the handler if it doesn't exist yet
|
|
if not fqm_logger.handlers:
|
|
# Set up the file handler
|
|
filehandler = RotatingFileHandler(
|
|
f"{splunkhome}/var/log/splunk/trackme_fqm_metrics.log",
|
|
mode="a",
|
|
maxBytes=100000000,
|
|
backupCount=1,
|
|
)
|
|
fqm_logger.addHandler(filehandler)
|
|
# Prevent propagation to root logger
|
|
fqm_logger.propagate = False
|
|
else:
|
|
# Find the RotatingFileHandler among existing handlers
|
|
filehandler = None
|
|
for handler in fqm_logger.handlers:
|
|
if isinstance(handler, RotatingFileHandler):
|
|
filehandler = handler
|
|
break
|
|
|
|
# If no RotatingFileHandler found, create one
|
|
if filehandler is None:
|
|
filehandler = RotatingFileHandler(
|
|
f"{splunkhome}/var/log/splunk/trackme_fqm_metrics.log",
|
|
mode="a",
|
|
maxBytes=100000000,
|
|
backupCount=1,
|
|
)
|
|
fqm_logger.addHandler(filehandler)
|
|
|
|
for metrics_item in metrics_list:
|
|
timestamp = float(metrics_item.get("time"))
|
|
metrics_item.pop("time") # Remove time field
|
|
|
|
# Update formatter with new timestamp
|
|
formatter = JSONFormatter(timestamp=timestamp)
|
|
filehandler.setFormatter(formatter)
|
|
|
|
# Build metrics_event dynamically from fields starting with "fields_quality."
|
|
metrics_event = {}
|
|
metrics_data = metrics_item.get("metrics", {})
|
|
for key, value in metrics_data.items():
|
|
if key.startswith("fields_quality."):
|
|
metrics_event[key] = value if value is not None else 0
|
|
|
|
fqm_logger.info(
|
|
"Metrics - group=fqm_metrics",
|
|
extra={
|
|
"target_index": metric_index,
|
|
"tenant_id": tenant_id,
|
|
"object": metrics_item.get("object"),
|
|
"object_id": metrics_item.get("object_id"),
|
|
"object_category": "splk-fqm",
|
|
"metrics_event": json.dumps(metrics_event),
|
|
},
|
|
)
|
|
|
|
except Exception as e:
|
|
raise Exception(str(e))
|
|
|
|
|
|
# return main searches logics for that entity
|
|
def splk_fqm_return_searches(tenant_id, fqm_type, entity_info):
|
|
# log debug
|
|
logging.debug(
|
|
f'Starting function=splk_fqm_return_searches with entity_info="{json.dumps(entity_info, indent=2)}"'
|
|
)
|
|
|
|
# define required searches dynamically based on the upstream entity information
|
|
splk_fqm_mctalog_search = None
|
|
splk_fqm_metrics_report = None
|
|
splk_fqm_mpreview = None
|
|
splk_fqm_metrics_populate_search = None
|
|
splk_fqm_chart_values_search = None
|
|
splk_fqm_chart_description_search = None
|
|
splk_fqm_chart_status_search = None
|
|
splk_fqm_table_summary_search = None
|
|
splk_fqm_table_summary_formated_search = None
|
|
splk_fqm_metrics_success_overtime = None
|
|
splk_fqm_search_sample_events = None
|
|
|
|
# metadata search constraint (set to * by default to avoid prevent results in case of no metadata fields)
|
|
metadata_search_constraint = "*"
|
|
|
|
# Extract metadata fields from fields_quality_summary to build search constraint
|
|
try:
|
|
if "fields_quality_summary" in entity_info and entity_info["fields_quality_summary"]:
|
|
# Parse the JSON string if it's a string, otherwise use as-is if it's already a dict
|
|
if isinstance(entity_info["fields_quality_summary"], str):
|
|
fields_quality_data = json.loads(entity_info["fields_quality_summary"])
|
|
else:
|
|
fields_quality_data = entity_info["fields_quality_summary"]
|
|
|
|
# Extract metadata fields and build constraint
|
|
metadata_constraints = []
|
|
for key, value in fields_quality_data.items():
|
|
if key.startswith("metadata."):
|
|
# Format as "metadata.fieldname"="value"
|
|
constraint = f'"{key}"="{value}"'
|
|
metadata_constraints.append(constraint)
|
|
|
|
# Join all constraints with spaces
|
|
if metadata_constraints:
|
|
metadata_search_constraint = " ".join(metadata_constraints)
|
|
|
|
except (json.JSONDecodeError, KeyError, TypeError) as e:
|
|
logging.warning(f"Failed to extract metadata constraints from fields_quality_summary: {str(e)}")
|
|
pass
|
|
|
|
try:
|
|
########
|
|
# mstats
|
|
########
|
|
|
|
# mcatalog
|
|
splk_fqm_mctalog_search = remove_leading_spaces(
|
|
f"""\
|
|
| mcatalog values(metric_name) as metrics, values(_dims) as dims where `trackme_metrics_idx({tenant_id})` tenant_id="{tenant_id}" object_category="splk-fqm" object_id="{entity_info["_key"]}" metric_name=* by index
|
|
"""
|
|
)
|
|
|
|
# metrics report
|
|
splk_fqm_metrics_report = remove_leading_spaces(
|
|
f"""\
|
|
| mstats latest(_value) as latest_value, avg(_value) as avg_value, max(_value) as max_value, perc95(_value) as perc95_value, stdev(_value) as stdev_value where `trackme_metrics_idx({tenant_id})` metric_name=* tenant_id="{tenant_id}" object_category="splk-fqm" object_id="{entity_info["_key"]}" by index, object, metric_name
|
|
| foreach *_value [ eval <<FIELD>> = if(match(metric_name, "\\.status"), round('<<FIELD>>', 0), round('<<FIELD>>', 3)) ]
|
|
"""
|
|
)
|
|
|
|
# mpreview
|
|
splk_fqm_mpreview = remove_leading_spaces(
|
|
f"""\
|
|
| mpreview `trackme_metrics_idx({tenant_id})` filter="tenant_id={tenant_id} object_category="splk-fqm" object_id={entity_info["_key"]}"
|
|
"""
|
|
)
|
|
|
|
# metrics popuating search
|
|
splk_fqm_metrics_populate_search = remove_leading_spaces(
|
|
f"""\
|
|
| mcatalog values(metric_name) as metrics where `trackme_metrics_idx({tenant_id})` tenant_id="{tenant_id}" object_category="splk-fqm" object_id="{entity_info["_key"]}" metric_name=*
|
|
| mvexpand metrics
|
|
| rename metrics as metric_name
|
|
| rex field=metric_name "^trackme\\.splk\\.fqm\\.(?<label>.*)"
|
|
| eval order=if(metric_name=="trackme.splk.fqm.status", 0, 1)
|
|
| sort 0 order
|
|
| fields - order
|
|
"""
|
|
)
|
|
|
|
# charts successes and failures
|
|
splk_fqm_chart_values_search = remove_leading_spaces(
|
|
f"""\
|
|
index={entity_info.get("tracker_index")} sourcetype=trackme:fields_quality source="trackme:quality:{entity_info.get("tracker_name")}" {metadata_search_constraint} | sort 0 _time
|
|
| trackmefieldsqualityextract
|
|
| where fieldname="{entity_info.get("fieldname")}"
|
|
| fillnull value="null" value
|
|
| top limit=15 value
|
|
"""
|
|
)
|
|
|
|
# charts description
|
|
splk_fqm_chart_description_search = remove_leading_spaces(
|
|
f"""\
|
|
index={entity_info.get("tracker_index")} sourcetype=trackme:fields_quality source="trackme:quality:{entity_info.get("tracker_name")}" {metadata_search_constraint} | sort 0 _time
|
|
| trackmefieldsqualityextract
|
|
| where fieldname="{entity_info.get("fieldname")}"
|
|
| top limit=15 description
|
|
"""
|
|
)
|
|
|
|
# chart status
|
|
splk_fqm_chart_status_search = remove_leading_spaces(
|
|
f"""\
|
|
index={entity_info.get("tracker_index")} sourcetype=trackme:fields_quality source="trackme:quality:{entity_info.get("tracker_name")}" {metadata_search_constraint} | sort 0 _time
|
|
| trackmefieldsqualityextract
|
|
| where fieldname="{entity_info.get("fieldname")}"
|
|
| top status
|
|
"""
|
|
)
|
|
|
|
# table summary
|
|
if fqm_type == "global":
|
|
splk_fqm_table_summary_search = remove_leading_spaces(
|
|
f"""\
|
|
| inputlookup trackme_fqm_tenant_{tenant_id} where _key="{entity_info["_key"]}" | eval keyid=_key | fields fields_quality_summary *
|
|
| spath input=fields_quality_summary
|
|
| fields @fieldname, @fieldstatus, percent_success, success_fields, failed_fields, total_fields_checked, total_fields_failed, total_fields_passed, metadata.datamodel, metadata.index, metadata.sourcetype, count_total, count_success, count_failure
|
|
| rename "@*" as "*"
|
|
| foreach fieldstatus, percent_success, count* [ eval <<FIELD>> = mvindex('<<FIELD>>', 0) ]
|
|
| foreach success_fields, failed_fields [ eval <<FIELD>> = mvsort(split('<<FIELD>>', ",")) ]
|
|
"""
|
|
)
|
|
else:
|
|
splk_fqm_table_summary_search = remove_leading_spaces(
|
|
f"""\
|
|
| inputlookup trackme_fqm_tenant_{tenant_id} where _key="{entity_info["_key"]}" | eval keyid=_key | fields fields_quality_summary *
|
|
| spath input=fields_quality_summary
|
|
| eval quality_results_description=coalesce('quality_results_description{{}}', quality_results_description)
|
|
| fields - "quality_results_description{{}}"
|
|
| fields @fieldname, recommended, @fieldstatus, percent_coverage, percent_success, metadata.index, metadata.sourcetype, count_total, count_success, count_failure, distinct_value_count, quality_results_description, field_values, regex_expression
|
|
| eval field_values=split(field_values, ",")
|
|
| rename "@*" as "*"
|
|
| foreach fieldstatus, percent_coverage, percent_success, count*, distinct_value_count [ eval <<FIELD>> = mvindex('<<FIELD>>', 0) ]
|
|
"""
|
|
)
|
|
|
|
# table summary formated
|
|
if fqm_type == "global":
|
|
splk_fqm_table_summary_formated_search = remove_leading_spaces(
|
|
f"""\
|
|
{splk_fqm_table_summary_search}
|
|
| eval fieldstatus=if(fieldstatus=="success", fieldstatus . " 🟢", fieldstatus . " 🔴")
|
|
| foreach percent_success [ eval <<FIELD>> = case('<<FIELD>>'==0, 0, '<<FIELD>>'==100, 100, 1=1, '<<FIELD>>') ]
|
|
"""
|
|
)
|
|
else:
|
|
splk_fqm_table_summary_formated_search = remove_leading_spaces(
|
|
f"""\
|
|
{splk_fqm_table_summary_search}
|
|
| lookup trackme_cim_recommended_fields field as fieldname OUTPUT is_recommended
|
|
| eval recommended=json_extract(w,"comment.recommended"), recommended=if(is_recommended=="true" OR match(recommended, "(?i)true|1"), "true", "false")
|
|
| eval fieldstatus=if(fieldstatus=="success", fieldstatus . " 🟢", fieldstatus . " 🔴"), recommended=if(recommended=="true", recommended . " ⭐", recommended)
|
|
| fields - is_recommended
|
|
| foreach percent_coverage, percent_success [ eval <<FIELD>> = case('<<FIELD>>'==0, 0, '<<FIELD>>'==100, 100, 1=1, '<<FIELD>>') ]
|
|
"""
|
|
)
|
|
|
|
# metrics success overtime
|
|
splk_fqm_metrics_success_overtime = remove_leading_spaces(
|
|
f"""\
|
|
| mstats min(_value) as value where `trackme_metrics_idx({tenant_id})` tenant_id="{tenant_id}" object_category="splk-fqm" object_id={entity_info["_key"]} metric_name=trackme.splk.fqm.fields_quality.percent_success by object, metric_name span=5m
|
|
| rex field=metric_name "^trackme.splk.fqm.(?<metric_name>.*)"
|
|
"""
|
|
)
|
|
|
|
# search sample events
|
|
if fqm_type == "global":
|
|
splk_fqm_search_sample_events_raw = remove_leading_spaces(
|
|
f"""\
|
|
index={entity_info.get("tracker_index")} sourcetype=trackme:fields_quality source="trackme:quality:{entity_info.get("tracker_name")}" {metadata_search_constraint} | sort 0 _time
|
|
| trackmefieldsqualityextract
|
|
"""
|
|
)
|
|
else:
|
|
splk_fqm_search_sample_events_raw = remove_leading_spaces(
|
|
f"""\
|
|
index={entity_info.get("tracker_index")} sourcetype=trackme:fields_quality source="trackme:quality:{entity_info.get("tracker_name")}" {metadata_search_constraint} | sort 0 _time
|
|
| trackmefieldsqualityextract
|
|
| where fieldname="{entity_info.get("fieldname")}"
|
|
"""
|
|
)
|
|
|
|
splk_fqm_search_sample_events = "search?q=" + urllib.parse.quote(
|
|
replace_encoded_doublebackslashes(splk_fqm_search_sample_events_raw)
|
|
)
|
|
|
|
# search sample not matching regex
|
|
if fqm_type == "global":
|
|
splk_fqm_search_sample_not_matching_regex_events_raw = remove_leading_spaces(
|
|
f"""\
|
|
index={entity_info.get("tracker_index")} sourcetype=trackme:fields_quality source="trackme:quality:{entity_info.get("tracker_name")}" {metadata_search_constraint}| sort 0 _time
|
|
| trackmefieldsqualityextract
|
|
| where description="Field exists but value does not match the required pattern."
|
|
| table _time, metadata.index, metadata.sourcetype, metadata.datamodel, metadata.nodename, fieldname, value, regex_expression
|
|
``` sort is mandatory to force all records to be retrieved before we call the gen summary command ```
|
|
| sort 0 _time
|
|
| trackmefieldsqualitygensummary maxvals=15 fieldvalues_format=csv groupby_metadata_fields="metadata.datamodel,metadata.nodename,metadata.index,metadata.sourcetype,fieldname"
|
|
| fields metadata.index, metadata.sourcetype, metadata.datamodel, metadata.nodename, fieldname, total_events, distinct_value_count, percent_coverage, field_values, regex_expression | fields - _time, _raw
|
|
"""
|
|
)
|
|
else:
|
|
splk_fqm_search_sample_not_matching_regex_events_raw = remove_leading_spaces(
|
|
f"""\
|
|
index={entity_info.get("tracker_index")} sourcetype=trackme:fields_quality source="trackme:quality:{entity_info.get("tracker_name")}" {metadata_search_constraint} | sort 0 _time
|
|
| trackmefieldsqualityextract
|
|
| where fieldname="{entity_info.get("fieldname")}"
|
|
| where description="Field exists but value does not match the required pattern."
|
|
| table _time, metadata.index, metadata.sourcetype, metadata.datamodel, metadata.nodename, fieldname, value, regex_expression
|
|
``` sort is mandatory to force all records to be retrieved before we call the gen summary command ```
|
|
| sort 0 _time
|
|
| trackmefieldsqualitygensummary maxvals=15 fieldvalues_format=csv groupby_metadata_fields="metadata.datamodel,metadata.nodename,metadata.index,metadata.sourcetype,fieldname"
|
|
| fields metadata.index, metadata.sourcetype, metadata.datamodel, metadata.nodename, fieldname, total_events, distinct_value_count, percent_coverage, field_values, regex_expression | fields - _time, _raw
|
|
"""
|
|
)
|
|
|
|
splk_fqm_search_sample_not_matching_regex_events = "search?q=" + urllib.parse.quote(
|
|
replace_encoded_doublebackslashes(splk_fqm_search_sample_not_matching_regex_events_raw)
|
|
)
|
|
|
|
response = {
|
|
"splk_fqm_mctalog_search": f"search?q={urllib.parse.quote(splk_fqm_mctalog_search)}",
|
|
"splk_fqm_mctalog_search_litsearch": splk_fqm_mctalog_search,
|
|
"splk_fqm_metrics_report": f"search?q={urllib.parse.quote(splk_fqm_metrics_report)}",
|
|
"splk_fqm_metrics_report_litsearch": splk_fqm_metrics_report,
|
|
"splk_fqm_mpreview": f"search?q={urllib.parse.quote(splk_fqm_mpreview)}",
|
|
"splk_fqm_mpreview_litsearch": splk_fqm_mpreview,
|
|
"splk_fqm_metrics_populate_search": splk_fqm_metrics_populate_search,
|
|
"splk_fqm_chart_values_search": splk_fqm_chart_values_search,
|
|
"splk_fqm_chart_description_search": splk_fqm_chart_description_search,
|
|
"splk_fqm_chart_status_search": splk_fqm_chart_status_search,
|
|
"splk_fqm_table_summary_search": splk_fqm_table_summary_search,
|
|
"splk_fqm_table_summary_formated_search": splk_fqm_table_summary_formated_search,
|
|
"splk_fqm_metrics_success_overtime": splk_fqm_metrics_success_overtime,
|
|
"splk_fqm_search_sample_events": splk_fqm_search_sample_events,
|
|
"splk_fqm_search_sample_events_raw": splk_fqm_search_sample_events_raw,
|
|
"splk_fqm_search_sample_not_matching_regex_events": splk_fqm_search_sample_not_matching_regex_events,
|
|
"splk_fqm_search_sample_not_matching_regex_events_raw": splk_fqm_search_sample_not_matching_regex_events_raw,
|
|
"metadata_search_constraint": metadata_search_constraint,
|
|
}
|
|
|
|
# return
|
|
return response
|
|
|
|
except Exception as e:
|
|
logging.error(
|
|
f'function splk_fqm_return_searches, an exception was encountered, exception="{str(e)}"'
|
|
)
|
|
raise Exception(e)
|