Additional is a Struct - https://protobuf.dev/reference/protobuf/google.protobuf/#struct - but I now understand you’re trying to add a Struct within the existing Struct :)
Here is the how Additional looks (using the BigQuery schema which calls a STRUCT a RECORD):
---
additional (RECORD)
-
fields (RECORD)
-
key (STRING)
-
value (RECORD)
-
string_value (STRING)
-
bool_value (BOOLEAN)
-
number_value (FLOAT)
---
So if you had an input log like below:
{
"timestamp": "2023-10-27T10:00:00Z",
"process": "Updater.exe",
"field": {
"Name": "MaxConnections",
"OldValue": 100,
"NewValue": 500
},
"action": "WriteProperty"
}
Then you could use GROK code as follows:
filter {
# 1. Initialize variables to empty strings to prevent "field not found" errors
mutate {
replace => {
"extracted_field_name" => ""
"extracted_old_value_str" => ""
"extracted_new_value_str" => ""
"json_parse_failed_flag" => "" # Flag to indicate if JSON parsing failed, will be "true" on error
"temp_old_value_struct_field" => "" # Temporary variable for the OldValue struct field
"temp_new_value_struct_field" => "" # Temporary variable for the NewValue struct field
"temp_main_additional_field_item" => "" # Temporary variable for the main item to be added to additional.fields
}
}
# 2. Parse the incoming message, assuming it is in JSON format
json {
source => "message"
array_function => "split_columns" # Useful for handling potential top-level JSON arrays
on_error => "json_parse_failed_flag" # Set this flag to "true" if JSON parsing fails
}
# 3. Proceed only if JSON parsing was successful (i.e., the error flag is NOT "true")
if [json_parse_failed_flag] != "true" {
# 4. Extract the 'Name' from the 'field' object
if [field][Name] != "" {
mutate {
replace => { "extracted_field_name" => "%{field.Name}" }
}
}
# 5. Convert 'OldValue' from integer to string and store
if [field][OldValue] != "" {
mutate { convert => { "field.OldValue" => "string" } }
mutate { replace => { "extracted_old_value_str" => "%{field.OldValue}" } }
}
# 5. Convert 'NewValue' from integer to string and store
if [field][NewValue] != "" {
mutate { convert => { "field.NewValue" => "string" } }
mutate { replace => { "extracted_new_value_str" => "%{field.NewValue}" } }
}
# 6. Construct the nested structure for additional.fields if the field name was extracted
if [extracted_field_name] != "" {
# Create the inner struct field for 'NewValue' (comes first in desired output)
mutate {
replace => {
"temp_new_value_struct_field.key" => "NewValue"
"temp_new_value_struct_field.value.string_value" => "%{extracted_new_value_str}"
}
}
# Create the inner struct field for 'OldValue'
mutate {
replace => {
"temp_old_value_struct_field.key" => "OldValue"
"temp_old_value_struct_field.value.string_value" => "%{extracted_old_value_str}"
}
}
# Initialize the main temporary variable for the additional.fields item
# This will hold the structure: { "key": "MaxConnections", "value": { "struct_value": { "fields": [...] } } }
mutate {
replace => {
"temp_main_additional_field_item.key" => "%{extracted_field_name}"
}
}
# Merge the individual struct fields into the 'fields' array within 'struct_value'
# The order matters here for the desired UDM output: NewValue then OldValue.
mutate {
merge => {
"temp_main_additional_field_item.value.struct_value.fields" => "temp_new_value_struct_field"
}
}
mutate {
merge => {
"temp_main_additional_field_item.value.struct_value.fields" => "temp_old_value_struct_field"
}
}
# 7. Finally, merge the complete custom item into the UDM's 'additional.fields'
mutate {
merge => {
"event.idm.read_only_udm.additional.fields" => "temp_main_additional_field_item"
}
}
}
}
# 8. Output the constructed event to Chronicle UDM
mutate {
merge => {
"@output" => "event"
}
}
}
Which would give you an output as follows:
idm:
readOnlyUdm:
additional:
MaxConnections:
OldValue: "100"
NewValue: "500"