Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .chloggen/windowseventlog-event-data-format.yaml
Original file line number Diff line number Diff line change
@@ -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 <Data> elements become direct keys (e.g., body["event_data"]["ProcessId"]).
Anonymous <Data> 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]
5 changes: 5 additions & 0 deletions pkg/stanza/operator/input/windows/config.schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ $defs:
properties:
channel:
type: string
event_data_format:
$ref: event_data_format
exclude_providers:
type: array
items:
Expand Down Expand Up @@ -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
Expand Down
38 changes: 26 additions & 12 deletions pkg/stanza/operator/input/windows/config_all.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions pkg/stanza/operator/input/windows/config_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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`")
Expand All @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion pkg/stanza/operator/input/windows/input.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type Input struct {
currentMaxReads int
startAt string
raw bool
eventDataFormat EventDataFormat
includeLogRecordOriginal bool
excludeProviders map[string]struct{}
pollInterval time.Duration
Expand Down Expand Up @@ -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)
Expand Down
40 changes: 29 additions & 11 deletions pkg/stanza/operator/input/windows/xml.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
}

Expand Down Expand Up @@ -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
}
Expand All @@ -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
}

Expand Down
Loading
Loading