diff --git a/.chloggen/windowseventlog-event-data-format.yaml b/.chloggen/windowseventlog-event-data-format.yaml new file mode 100644 index 0000000000000..67dc943dbdbf0 --- /dev/null +++ b/.chloggen/windowseventlog-event-data-format.yaml @@ -0,0 +1,31 @@ +# Use this changelog template to create an entry for release notes. + +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: breaking + +# The name of the component, or a single word describing the area of concern, (e.g. receiver/filelog) +component: receiver/windowseventlog + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: "Change event_data from an array of single-key maps to a flat map by default, making fields directly accessible via OTTL. The previous format is available by setting `event_data_format: array`." + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [42565, 32952] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: | + Named elements become direct keys (e.g., body["event_data"]["ProcessId"]). + Anonymous elements use numbered keys: param1, param2, etc. + To preserve the previous array format, set event_data_format: array in the receiver configuration. + +# If your change doesn't affect end users or the exported elements of any package, +# you should instead start your pull request title with [chore] or use the "Skip Changelog" +# label. +# Optional: The change log or logs in which this entry should be included. +# e.g. '[user]' or '[user, api]' +# Include 'user' if the change is relevant to end users. +# Include 'api' if there is a change to a library API. +# Default: '[user]' +change_logs: [user] diff --git a/pkg/stanza/operator/input/windows/config.schema.yaml b/pkg/stanza/operator/input/windows/config.schema.yaml index 0a8af6a65d99a..bc1588a0277df 100644 --- a/pkg/stanza/operator/input/windows/config.schema.yaml +++ b/pkg/stanza/operator/input/windows/config.schema.yaml @@ -5,6 +5,8 @@ $defs: properties: channel: type: string + event_data_format: + $ref: event_data_format exclude_providers: type: array items: @@ -33,6 +35,9 @@ $defs: type: boolean allOf: - $ref: github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza/operator/helper.input_config + event_data_format: + description: EventDataFormat controls the structure of the event_data field in the log body. + type: string remote_config: description: RemoteConfig is the configuration for a remote server. type: object diff --git a/pkg/stanza/operator/input/windows/config_all.go b/pkg/stanza/operator/input/windows/config_all.go index 175eef3a22bc8..58d8803285805 100644 --- a/pkg/stanza/operator/input/windows/config_all.go +++ b/pkg/stanza/operator/input/windows/config_all.go @@ -11,6 +11,18 @@ import ( const operatorType = "windows_eventlog_input" +// EventDataFormat controls the structure of the event_data field in the log body. +type EventDataFormat string + +const ( + // EventDataFormatMap emits event_data as a flat map with named Data elements + // as direct keys and anonymous Data elements numbered as param1, param2, etc. + EventDataFormatMap EventDataFormat = "map" + // EventDataFormatArray emits event_data with a nested "data" array of + // single-key maps, preserving the original format. + EventDataFormatArray EventDataFormat = "array" +) + // NewConfig will return an event log config with default values. func NewConfig() *Config { return NewConfigWithID(operatorType) @@ -24,24 +36,26 @@ func NewConfigWithID(operatorID string) *Config { StartAt: "end", PollInterval: 1 * time.Second, IgnoreChannelErrors: false, + EventDataFormat: EventDataFormatMap, } } // Config is the configuration of a windows event log operator. type Config struct { helper.InputConfig `mapstructure:",squash"` - Channel string `mapstructure:"channel"` - IgnoreChannelErrors bool `mapstructure:"ignore_channel_errors,omitempty"` - MaxReads int `mapstructure:"max_reads,omitempty"` - StartAt string `mapstructure:"start_at,omitempty"` - PollInterval time.Duration `mapstructure:"poll_interval,omitempty"` - MaxEventsPerPoll int `mapstructure:"max_events_per_poll,omitempty"` - Raw bool `mapstructure:"raw,omitempty"` - IncludeLogRecordOriginal bool `mapstructure:"include_log_record_original,omitempty"` - SuppressRenderingInfo bool `mapstructure:"suppress_rendering_info,omitempty"` - ExcludeProviders []string `mapstructure:"exclude_providers,omitempty"` - Remote RemoteConfig `mapstructure:"remote,omitempty"` - Query *string `mapstructure:"query,omitempty"` + Channel string `mapstructure:"channel"` + IgnoreChannelErrors bool `mapstructure:"ignore_channel_errors,omitempty"` + MaxReads int `mapstructure:"max_reads,omitempty"` + StartAt string `mapstructure:"start_at,omitempty"` + PollInterval time.Duration `mapstructure:"poll_interval,omitempty"` + MaxEventsPerPoll int `mapstructure:"max_events_per_poll,omitempty"` + Raw bool `mapstructure:"raw,omitempty"` + EventDataFormat EventDataFormat `mapstructure:"event_data_format,omitempty"` + IncludeLogRecordOriginal bool `mapstructure:"include_log_record_original,omitempty"` + SuppressRenderingInfo bool `mapstructure:"suppress_rendering_info,omitempty"` + ExcludeProviders []string `mapstructure:"exclude_providers,omitempty"` + Remote RemoteConfig `mapstructure:"remote,omitempty"` + Query *string `mapstructure:"query,omitempty"` } // RemoteConfig is the configuration for a remote server. diff --git a/pkg/stanza/operator/input/windows/config_windows.go b/pkg/stanza/operator/input/windows/config_windows.go index e00c8c32f23d2..52418026e1d7f 100644 --- a/pkg/stanza/operator/input/windows/config_windows.go +++ b/pkg/stanza/operator/input/windows/config_windows.go @@ -40,6 +40,10 @@ func (c *Config) Build(set component.TelemetrySettings) (operator.Operator, erro return nil, errors.New("the `start_at` field must be set to `beginning` or `end`") } + if c.EventDataFormat != EventDataFormatMap && c.EventDataFormat != EventDataFormatArray { + return nil, errors.New("the `event_data_format` field must be set to `map` or `array`") + } + if (c.Remote.Server != "" || c.Remote.Username != "" || c.Remote.Password != "") && // any not empty (c.Remote.Server == "" || c.Remote.Username == "" || c.Remote.Password == "") { // any empty return nil, errors.New("remote configuration must have non-empty `username` and `password`") @@ -56,6 +60,7 @@ func (c *Config) Build(set component.TelemetrySettings) (operator.Operator, erro startAt: c.StartAt, pollInterval: c.PollInterval, raw: c.Raw, + eventDataFormat: c.EventDataFormat, includeLogRecordOriginal: c.IncludeLogRecordOriginal, excludeProviders: excludeProvidersSet(c.ExcludeProviders), remote: c.Remote, diff --git a/pkg/stanza/operator/input/windows/input.go b/pkg/stanza/operator/input/windows/input.go index 2632c86ea7d61..1ef847b26e50d 100644 --- a/pkg/stanza/operator/input/windows/input.go +++ b/pkg/stanza/operator/input/windows/input.go @@ -34,6 +34,7 @@ type Input struct { currentMaxReads int startAt string raw bool + eventDataFormat EventDataFormat includeLogRecordOriginal bool excludeProviders map[string]struct{} pollInterval time.Duration @@ -353,7 +354,7 @@ func (i *Input) processEventWithRenderingInfo(ctx context.Context, event Event) func (i *Input) sendEvent(ctx context.Context, eventXML *EventXML) error { var body any = eventXML.Original if !i.raw { - body = formattedBody(eventXML) + body = formattedBody(eventXML, i.eventDataFormat) } e, err := i.NewEntry(body) diff --git a/pkg/stanza/operator/input/windows/xml.go b/pkg/stanza/operator/input/windows/xml.go index a9ba0e1704b18..396e97bb1426b 100644 --- a/pkg/stanza/operator/input/windows/xml.go +++ b/pkg/stanza/operator/input/windows/xml.go @@ -75,7 +75,7 @@ func parseSeverity(renderedLevel, level string) entry.Severity { } // formattedBody will parse a body from the event. -func formattedBody(e *EventXML) map[string]any { +func formattedBody(e *EventXML, eventDataFormat EventDataFormat) map[string]any { message, details := parseMessage(e.Channel, e.Message) level := e.RenderedLevel @@ -117,7 +117,7 @@ func formattedBody(e *EventXML) map[string]any { "task": task, "opcode": opcode, "keywords": keywords, - "event_data": parseEventData(e.EventData), + "event_data": parseEventData(e.EventData, eventDataFormat), "version": e.Version, } @@ -152,10 +152,15 @@ func parseMessage(channel, message string) (string, map[string]any) { } } -// parse event data into a map[string]interface +// parseEventData converts EventData XML elements into a map. +// When format is EventDataFormatMap, named Data elements become direct keys and +// anonymous elements use numbered keys (param1, param2, …). +// When format is EventDataFormatArray, data is stored as a "data" slice of +// single-key maps, preserving the original collector format. // see: https://learn.microsoft.com/en-us/windows/win32/wes/eventschema-datafieldtype-complextype -func parseEventData(eventData EventData) map[string]any { - outputMap := make(map[string]any, 3) +func parseEventData(eventData EventData, format EventDataFormat) map[string]any { + outputMap := make(map[string]any, len(eventData.Data)+2) + if eventData.Name != "" { outputMap["name"] = eventData.Name } @@ -167,15 +172,28 @@ func parseEventData(eventData EventData) map[string]any { return outputMap } - dataMaps := make([]any, len(eventData.Data)) - for i, data := range eventData.Data { - dataMaps[i] = map[string]any{ - data.Name: data.Value, + switch format { + case EventDataFormatArray: + dataMaps := make([]any, len(eventData.Data)) + for i, data := range eventData.Data { + dataMaps[i] = map[string]any{ + data.Name: data.Value, + } + } + outputMap["data"] = dataMaps + default: + anonymousCounter := 1 + for _, data := range eventData.Data { + if data.Name != "" { + outputMap[data.Name] = data.Value + } else { + key := fmt.Sprintf("param%d", anonymousCounter) + outputMap[key] = data.Value + anonymousCounter++ + } } } - outputMap["data"] = dataMaps - return outputMap } diff --git a/pkg/stanza/operator/input/windows/xml_test.go b/pkg/stanza/operator/input/windows/xml_test.go index 621bffcc07ec7..cf0d738043c78 100644 --- a/pkg/stanza/operator/input/windows/xml_test.go +++ b/pkg/stanza/operator/input/windows/xml_test.go @@ -93,15 +93,13 @@ func TestParseBody(t *testing.T) { "opcode": "rendered_opcode", "keywords": []string{"RenderedKeywords"}, "event_data": map[string]any{ - "data": []any{ - map[string]any{"1st_name": "value"}, - map[string]any{"2nd_name": "another_value"}, - }, + "1st_name": "value", + "2nd_name": "another_value", }, "version": uint8(0), } - require.Equal(t, expected, formattedBody(xml)) + require.Equal(t, expected, formattedBody(xml, EventDataFormatMap)) } func TestParseBodySecurityExecution(t *testing.T) { @@ -170,15 +168,13 @@ func TestParseBodySecurityExecution(t *testing.T) { "user_id": "my-user-id", }, "event_data": map[string]any{ - "data": []any{ - map[string]any{"name": "value"}, - map[string]any{"another_name": "another_value"}, - }, + "name": "value", + "another_name": "another_value", }, "version": uint8(0), } - require.Equal(t, expected, formattedBody(xml)) + require.Equal(t, expected, formattedBody(xml, EventDataFormatMap)) } func TestParseBodyFullExecution(t *testing.T) { @@ -263,15 +259,13 @@ func TestParseBodyFullExecution(t *testing.T) { "user_id": "my-user-id", }, "event_data": map[string]any{ - "data": []any{ - map[string]any{"name": "value"}, - map[string]any{"another_name": "another_value"}, - }, + "name": "value", + "another_name": "another_value", }, "version": uint8(0), } - require.Equal(t, expected, formattedBody(xml)) + require.Equal(t, expected, formattedBody(xml, EventDataFormatMap)) } func TestParseBodyCorrelation(t *testing.T) { @@ -332,10 +326,8 @@ func TestParseBodyCorrelation(t *testing.T) { "opcode": "rendered_opcode", "keywords": []string{"RenderedKeywords"}, "event_data": map[string]any{ - "data": []any{ - map[string]any{"1st_name": "value"}, - map[string]any{"2nd_name": "another_value"}, - }, + "1st_name": "value", + "2nd_name": "another_value", }, "correlation": map[string]any{ "activity_id": "{11111111-1111-1111-1111-111111111111}", @@ -344,7 +336,7 @@ func TestParseBodyCorrelation(t *testing.T) { "version": uint8(1), } - require.Equal(t, expected, formattedBody(xml)) + require.Equal(t, expected, formattedBody(xml, EventDataFormatMap)) } func TestParseNoRendered(t *testing.T) { @@ -395,15 +387,13 @@ func TestParseNoRendered(t *testing.T) { "opcode": "opcode", "keywords": []string{"keyword"}, "event_data": map[string]any{ - "data": []any{ - map[string]any{"name": "value"}, - map[string]any{"another_name": "another_value"}, - }, + "name": "value", + "another_name": "another_value", }, "version": uint8(0), } - require.Equal(t, expected, formattedBody(xml)) + require.Equal(t, expected, formattedBody(xml, EventDataFormatMap)) } func TestParseBodySecurity(t *testing.T) { @@ -458,50 +448,44 @@ func TestParseBodySecurity(t *testing.T) { "opcode": "rendered_opcode", "keywords": []string{"RenderedKeywords"}, "event_data": map[string]any{ - "data": []any{ - map[string]any{"name": "value"}, - map[string]any{"another_name": "another_value"}, - }, + "name": "value", + "another_name": "another_value", }, "version": uint8(0), } - require.Equal(t, expected, formattedBody(xml)) + require.Equal(t, expected, formattedBody(xml, EventDataFormatMap)) } func TestParseEventData(t *testing.T) { xmlMap := &EventXML{ EventData: EventData{ Name: "EVENT_DATA", - Data: []Data{{Name: "name", Value: "value"}}, + Data: []Data{{Name: "field", Value: "value"}}, Binary: "2D20", }, } - parsed := formattedBody(xmlMap) + parsed := formattedBody(xmlMap, EventDataFormatMap) expectedMap := map[string]any{ - "name": "EVENT_DATA", - "data": []any{ - map[string]any{"name": "value"}, - }, + "name": "EVENT_DATA", + "field": "value", "binary": "2D20", } require.Equal(t, expectedMap, parsed["event_data"]) xmlMixed := &EventXML{ EventData: EventData{ - Data: []Data{{Name: "name", Value: "value"}, {Value: "no_name"}}, + Data: []Data{{Name: "named_field", Value: "value"}, {Value: "no_name"}}, }, } - parsed = formattedBody(xmlMixed) - expectedSlice := map[string]any{ - "data": []any{ - map[string]any{"name": "value"}, - map[string]any{"": "no_name"}, - }, + parsed = formattedBody(xmlMixed, EventDataFormatMap) + expectedFlat := map[string]any{ + "named_field": "value", + "param1": "no_name", } - require.Equal(t, expectedSlice, parsed["event_data"]) + require.Equal(t, expectedFlat, parsed["event_data"]) } func TestInvalidUnmarshal(t *testing.T) { @@ -873,3 +857,291 @@ func TestUnmarshalWithUserData(t *testing.T) { require.Equal(t, xml, event) } + +func TestParseEventDataVariants(t *testing.T) { + tests := []struct { + name string + input EventData + expected map[string]any + }{ + { + name: "all named", + input: EventData{ + Data: []Data{ + {Name: "ProcessId", Value: "7924"}, + {Name: "Application", Value: "app.exe"}, + }, + }, + expected: map[string]any{ + "ProcessId": "7924", + "Application": "app.exe", + }, + }, + { + name: "all anonymous", + input: EventData{ + Data: []Data{ + {Value: "first"}, + {Value: "second"}, + }, + }, + expected: map[string]any{ + "param1": "first", + "param2": "second", + }, + }, + { + name: "mixed named and anonymous", + input: EventData{ + Data: []Data{ + {Name: "Named1", Value: "value1"}, + {Value: "anonymous1"}, + {Name: "Named2", Value: "value2"}, + {Value: "anonymous2"}, + }, + }, + expected: map[string]any{ + "Named1": "value1", + "param1": "anonymous1", + "Named2": "value2", + "param2": "anonymous2", + }, + }, + { + name: "with name and binary attributes", + input: EventData{ + Name: "EVENT_DATA", + Binary: "2D20", + Data: []Data{ + {Name: "Field", Value: "value"}, + }, + }, + expected: map[string]any{ + "name": "EVENT_DATA", + "binary": "2D20", + "Field": "value", + }, + }, + { + name: "empty event data", + input: EventData{}, + expected: map[string]any{}, + }, + { + name: "duplicate named keys - last wins", + input: EventData{ + Data: []Data{ + {Name: "Key", Value: "first"}, + {Name: "Key", Value: "second"}, + }, + }, + expected: map[string]any{ + "Key": "second", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := parseEventData(tt.input, EventDataFormatMap) + require.Equal(t, tt.expected, result) + }) + } +} + +func TestParseEventDataArrayFormat(t *testing.T) { + tests := []struct { + name string + input EventData + expected map[string]any + }{ + { + name: "named data as array", + input: EventData{ + Data: []Data{ + {Name: "ProcessId", Value: "7924"}, + {Name: "Application", Value: "app.exe"}, + }, + }, + expected: map[string]any{ + "data": []any{ + map[string]any{"ProcessId": "7924"}, + map[string]any{"Application": "app.exe"}, + }, + }, + }, + { + name: "anonymous data as array", + input: EventData{ + Data: []Data{ + {Value: "first"}, + {Value: "second"}, + }, + }, + expected: map[string]any{ + "data": []any{ + map[string]any{"": "first"}, + map[string]any{"": "second"}, + }, + }, + }, + { + name: "with name and binary attributes", + input: EventData{ + Name: "EVENT_DATA", + Binary: "2D20", + Data: []Data{ + {Name: "Field", Value: "value"}, + }, + }, + expected: map[string]any{ + "name": "EVENT_DATA", + "binary": "2D20", + "data": []any{ + map[string]any{"Field": "value"}, + }, + }, + }, + { + name: "empty event data", + input: EventData{}, + expected: map[string]any{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := parseEventData(tt.input, EventDataFormatArray) + require.Equal(t, tt.expected, result) + }) + } +} + +func TestParseBodyWithAnonymousEventData(t *testing.T) { + xml := &EventXML{ + EventID: EventID{ + ID: 1, + Qualifiers: 2, + }, + Provider: Provider{ + Name: "provider", + }, + TimeCreated: TimeCreated{ + SystemTime: "2020-07-30T01:01:01.123456789Z", + }, + Computer: "computer", + Channel: "application", + RecordID: 1, + Level: "Information", + Message: "message", + Task: "task", + Opcode: "opcode", + Keywords: []string{"keyword"}, + EventData: EventData{ + Data: []Data{{Value: "first_value"}, {Value: "second_value"}}, + Binary: "2D20", + }, + Version: 0, + } + + body := formattedBody(xml, EventDataFormatMap) + eventData := body["event_data"].(map[string]any) + + require.Equal(t, "first_value", eventData["param1"]) + require.Equal(t, "second_value", eventData["param2"]) + require.Equal(t, "2D20", eventData["binary"]) +} + +func TestFormattedBodyArrayFormat(t *testing.T) { + xml := &EventXML{ + EventID: EventID{ + ID: 1, + Qualifiers: 2, + }, + Provider: Provider{ + Name: "provider", + GUID: "guid", + EventSourceName: "event source", + }, + TimeCreated: TimeCreated{ + SystemTime: "2020-07-30T01:01:01.123456789Z", + }, + Computer: "computer", + Channel: "application", + RecordID: 1, + Level: "Information", + Message: "message", + Task: "task", + Opcode: "opcode", + Keywords: []string{"keyword"}, + EventData: EventData{ + Data: []Data{{Name: "ProcessId", Value: "7924"}, {Name: "Application", Value: "app.exe"}}, + }, + RenderedLevel: "rendered_level", + RenderedTask: "rendered_task", + RenderedOpcode: "rendered_opcode", + RenderedKeywords: []string{"RenderedKeywords"}, + Version: 0, + } + + body := formattedBody(xml, EventDataFormatArray) + eventData := body["event_data"].(map[string]any) + + expected := []any{ + map[string]any{"ProcessId": "7924"}, + map[string]any{"Application": "app.exe"}, + } + require.Equal(t, expected, eventData["data"]) +} + +func TestUnmarshalAndFormatAnonymousEventData(t *testing.T) { + data, err := os.ReadFile(filepath.Join("testdata", "xmlWithAnonymousEventDataEntries.xml")) + require.NoError(t, err) + + event, err := unmarshalEventXML(data) + require.NoError(t, err) + + mapBody := formattedBody(event, EventDataFormatMap) + mapEventData := mapBody["event_data"].(map[string]any) + require.Equal(t, "1st_value", mapEventData["param1"]) + require.Equal(t, "2nd_value", mapEventData["param2"]) + require.Equal(t, "2D20", mapEventData["binary"]) + _, hasDataKey := mapEventData["data"] + require.False(t, hasDataKey, "map format should not have a 'data' key") + + arrayBody := formattedBody(event, EventDataFormatArray) + arrayEventData := arrayBody["event_data"].(map[string]any) + require.Equal(t, []any{ + map[string]any{"": "1st_value"}, + map[string]any{"": "2nd_value"}, + }, arrayEventData["data"]) + require.Equal(t, "2D20", arrayEventData["binary"]) +} + +func TestUnmarshalAndFormatNamedEventData(t *testing.T) { + data, err := os.ReadFile(filepath.Join("testdata", "xmlSample.xml")) + require.NoError(t, err) + + event, err := unmarshalEventXML(data) + require.NoError(t, err) + + mapBody := formattedBody(event, EventDataFormatMap) + mapEventData := mapBody["event_data"].(map[string]any) + require.Equal(t, "2022-04-28T19:48:52Z", mapEventData["Time"]) + require.Equal(t, "RulesEngine", mapEventData["Source"]) + + arrayBody := formattedBody(event, EventDataFormatArray) + arrayEventData := arrayBody["event_data"].(map[string]any) + require.Equal(t, []any{ + map[string]any{"Time": "2022-04-28T19:48:52Z"}, + map[string]any{"Source": "RulesEngine"}, + }, arrayEventData["data"]) +} + +func TestParseEventDataSingleAnonymous(t *testing.T) { + input := EventData{ + Data: []Data{{Value: "Test log"}}, + } + result := parseEventData(input, EventDataFormatMap) + require.Equal(t, map[string]any{"param1": "Test log"}, result) +} diff --git a/receiver/windowseventlogreceiver/README.md b/receiver/windowseventlogreceiver/README.md index 4cd87ab321bd1..d802453f9ced2 100644 --- a/receiver/windowseventlogreceiver/README.md +++ b/receiver/windowseventlogreceiver/README.md @@ -30,6 +30,7 @@ Tails and parses logs from windows event log API using the [opentelemetry-log-co | `resource` | {} | A map of `key: value` pairs to add to the entry's resource. | | `operators` | [] | An array of [operators](https://github.com/open-telemetry/opentelemetry-log-collection/blob/main/docs/operators/README.md#what-operators-are-available). See below for more details | | `raw` | false | If false, the body of emitted log records will contain a structured representation of the event. Otherwise, the body will be the original XML string. | +| `event_data_format` | `map` | Controls the structure of the `event_data` field when `raw` is false. `map` emits a flat map (named elements as direct keys, anonymous elements as `param1`, `param2`, etc.). `array` emits the legacy format with a nested `data` array of single-key maps. | | `include_log_record_original` | false | If false, no additional attributes are added. If true, `log.record.original` is added to the attributes, which stores the original XML string according to the configured `suppress_rendering_info` (see below). | `suppress_rendering_info` | false | If false, [additional syscalls](https://learn.microsoft.com/en-us/windows/win32/api/winevt/nf-winevt-evtformatmessage#remarks) may be made to retrieve detailed information about the event. Otherwise, some unresolved values may be present in the event. | | `exclude_providers` | [] | One or more event log providers to exclude from processing. | @@ -83,6 +84,11 @@ Output entry sample: "id": 10, "qualifiers": 0 }, + "event_data": + { + "ProcessId": "7924", + "Application": "app.exe" + }, "keywords": "[Classic]", "level": "Information", "message": "Test log", @@ -99,6 +105,22 @@ Output entry sample: } ``` +The `event_data` field format is controlled by the `event_data_format` setting: + +**`event_data_format: map`** (default) — Named `` elements become direct keys (e.g., `event_data.ProcessId`). Anonymous `` elements (without a `Name` attribute) use numbered keys: `param1`, `param2`, etc. Fields are directly accessible via OTTL: `body["event_data"]["ProcessId"]`. + +**`event_data_format: array`** — Preserves the legacy format where data is stored as a nested `data` array of single-key maps: +```json +{ + "event_data": { + "data": [ + {"ProcessId": "7924"}, + {"Application": "app.exe"} + ] + } +} +``` + #### Remote Configuration If collection of the local event log is desired, a separate receiver needs to be created. diff --git a/receiver/windowseventlogreceiver/receiver_windows_test.go b/receiver/windowseventlogreceiver/receiver_windows_test.go index 977133f88c5c0..1734d9e48b62b 100644 --- a/receiver/windowseventlogreceiver/receiver_windows_test.go +++ b/receiver/windowseventlogreceiver/receiver_windows_test.go @@ -162,7 +162,7 @@ func TestReadWindowsEventLogger(t *testing.T) { eventDataMap, ok := eventData.(map[string]any) require.True(t, ok) require.Equal(t, map[string]any{ - "data": []any{map[string]any{"": "Test log"}}, + "param1": "Test log", }, eventDataMap) eventID := body["event_id"] @@ -215,7 +215,7 @@ func TestReadWindowsEventLoggerWithQuery(t *testing.T) { eventDataMap, ok := eventData.(map[string]any) require.True(t, ok) require.Equal(t, map[string]any{ - "data": []any{map[string]any{"": "Test log"}}, + "param1": "Test log", }, eventDataMap) eventID := body["event_id"]