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.
186 lines
5.6 KiB
186 lines
5.6 KiB
#!/usr/bin/env python
|
|
import sys
|
|
import json
|
|
import re
|
|
from distutils.util import strtobool
|
|
from splunk_instrumentation.splunklib.searchcommands import (
|
|
StreamingCommand,
|
|
Configuration,
|
|
Option,
|
|
dispatch
|
|
)
|
|
|
|
FIELD_NAME_REGEX = "[_a-zA-Z0-9\-\. ]*"
|
|
FIELDS_REGEX = '([_a-zA-Z0-9\-\. \*]+)((?i:\((json|string|int|float|bool)\)|\[(json|string|int|float|bool)?\]))?'
|
|
ERROR_INVALID_FIELD_NAME = "Invalid field name %s. Fields must be expressed in the format '" + FIELDS_REGEX + "'."
|
|
ERROR_FIELD_PATH_CONFLICT = "Can't create field %s due to conflict."
|
|
|
|
|
|
def convert_field(val, field_type, default=None):
|
|
try:
|
|
if field_type == STRING:
|
|
return val
|
|
elif field_type == INT:
|
|
return int(val)
|
|
elif field_type == FLOAT:
|
|
return float(val)
|
|
elif field_type == BOOL:
|
|
return bool(strtobool(val))
|
|
elif field_type == JSON:
|
|
return json.loads(val)
|
|
else:
|
|
# auto detect type if none is specified, defaults to string
|
|
convert_val = convert_field(val, INT)
|
|
if convert_val is not None:
|
|
return convert_val
|
|
|
|
convert_val = convert_field(val, FLOAT)
|
|
if convert_val is not None:
|
|
return convert_val
|
|
|
|
return val
|
|
|
|
except ValueError: # return default if forced conversion fails
|
|
return default
|
|
|
|
|
|
def convert_list(vals, field_type):
|
|
res = []
|
|
for idx, val in enumerate(vals):
|
|
res.append(convert_field(val, field_type, default=val))
|
|
return res
|
|
|
|
|
|
AUTO, JSON, INT, STRING, FLOAT, BOOL = ["AUTO", "JSON", "INT", "STRING", "FLOAT", "BOOL"]
|
|
|
|
|
|
@Configuration()
|
|
class MakeJsonCommand(StreamingCommand):
|
|
_validFields = [{
|
|
"regex": re.compile(FIELD_NAME_REGEX),
|
|
"type": AUTO,
|
|
"forceArray": False
|
|
}]
|
|
|
|
output = Option(
|
|
doc="""
|
|
Name of field that contains the JSON output.
|
|
""",
|
|
require=True)
|
|
|
|
def get_field_type(self, field):
|
|
for validField in self._validFields:
|
|
if validField["regex"].match(field):
|
|
return validField["type"], validField["forceArray"]
|
|
|
|
return None, None
|
|
|
|
def get_json(self, data):
|
|
res = {}
|
|
|
|
for k, v in data.items():
|
|
field_type, force_array = self.get_field_type(k)
|
|
if field_type:
|
|
dotpath = k.split(".")
|
|
|
|
# Setup the correct amount of nested dicts based on the dot path
|
|
target = res
|
|
for segment in dotpath[:-1]:
|
|
target.setdefault(segment, {})
|
|
target = target[segment]
|
|
|
|
try:
|
|
if isinstance(v, list):
|
|
target[dotpath[-1]] = convert_list(v, field_type)
|
|
elif v is not None:
|
|
converted_val = convert_field(v, field_type, default=v)
|
|
target[dotpath[-1]] = [
|
|
converted_val] if force_array else \
|
|
converted_val
|
|
else:
|
|
target[dotpath[-1]] = None
|
|
|
|
except TypeError:
|
|
raise Exception(ERROR_FIELD_PATH_CONFLICT % k)
|
|
return res
|
|
|
|
def set_valid_fields(self):
|
|
errors = []
|
|
self._validFields = []
|
|
|
|
for jsonField in self.fieldnames:
|
|
match = re.search("^" + FIELDS_REGEX + "$", jsonField)
|
|
|
|
if match and match.group(1):
|
|
field_type = match.group(2)
|
|
|
|
if field_type:
|
|
force_array = (
|
|
field_type[:1] == "[") # force conversion to array
|
|
field_type = field_type[1:-1].upper()
|
|
if field_type == "":
|
|
field_type = AUTO
|
|
else:
|
|
force_array = False
|
|
field_type = AUTO
|
|
|
|
regex_pattern = match.group(1).replace(".", "\\.") \
|
|
.replace("*", FIELD_NAME_REGEX)
|
|
|
|
self._validFields.append({
|
|
"type": field_type,
|
|
"forceArray": force_array,
|
|
"regex": re.compile("^" + regex_pattern + "$")
|
|
|
|
})
|
|
|
|
else:
|
|
errors.append(ERROR_INVALID_FIELD_NAME % jsonField)
|
|
|
|
if len(self._validFields) == 0:
|
|
self._validFields.append({
|
|
"regex": re.compile(FIELD_NAME_REGEX),
|
|
"type": AUTO,
|
|
"forceArray": False
|
|
})
|
|
|
|
return errors
|
|
|
|
def prepare(self):
|
|
errors = self.set_valid_fields()
|
|
for error in errors:
|
|
self.write_error(error)
|
|
|
|
if len(errors) > 0:
|
|
self.error_exit(Exception("Fieldname validation failed "
|
|
"for makejson command."))
|
|
|
|
def stream(self, results):
|
|
error_counts = {}
|
|
found_results = 0
|
|
|
|
for res in results:
|
|
found_results += 1
|
|
try:
|
|
json_val = self.get_json(res)
|
|
res[self.output] = json.dumps(json_val)
|
|
except Exception as e:
|
|
if e.message in error_counts:
|
|
error_counts[e.message] += 1
|
|
else:
|
|
error_counts[e.message] = 1
|
|
|
|
res[self.output] = "{}"
|
|
|
|
yield res
|
|
|
|
# report all errors
|
|
for err in error_counts:
|
|
if error_counts[err] > 0:
|
|
self.write_error(
|
|
err + " (" + str(error_counts[err]) + " of " + str(
|
|
found_results) + " events)")
|
|
|
|
|
|
dispatch(command_class=MakeJsonCommand, argv=sys.argv, module_name= __name__)
|