From da9d94cdacb6329d231a2687c51514719c4368ac Mon Sep 17 00:00:00 2001 From: Daniel Jaglowski Date: Thu, 26 Sep 2024 22:28:31 -0400 Subject: [PATCH] [receiver/windowseventlog] Add suppress_rendering_info parameter and simplify internal logic. (#34720) **Description:** This PR contains several changes described in #34131. It does not go as far as breaking out a separate parsing component, but I think it is enough to satisfy the known use cases. - Add `suppress_rendering_info` parameter, which acts orthogonally to `raw` flag. - Remove `RemoteServer` field from `EventXML`. Instead, set `attributes["remote_server"]` if remote collection is used. **Link to tracking Issue:** Resolves https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/34131 --- .chloggen/wel-supress-rendering-info.yaml | 29 +++ .chloggen/wel-supress-rendering-info2.yaml | 33 +++ .chloggen/wel-supress-rendering-info3.yaml | 27 +++ .../docs/operators/windows_eventlog_input.md | 2 + .../operator/input/windows/config_all.go | 17 +- .../operator/input/windows/config_windows.go | 16 +- pkg/stanza/operator/input/windows/event.go | 58 +++-- pkg/stanza/operator/input/windows/input.go | 120 ++++++----- pkg/stanza/operator/input/windows/xml.go | 62 +++--- pkg/stanza/operator/input/windows/xml_test.go | 201 +++--------------- receiver/windowseventlogreceiver/README.md | 5 +- 11 files changed, 259 insertions(+), 311 deletions(-) create mode 100644 .chloggen/wel-supress-rendering-info.yaml create mode 100644 .chloggen/wel-supress-rendering-info2.yaml create mode 100644 .chloggen/wel-supress-rendering-info3.yaml diff --git a/.chloggen/wel-supress-rendering-info.yaml b/.chloggen/wel-supress-rendering-info.yaml new file mode 100644 index 000000000000..9443c9b92d82 --- /dev/null +++ b/.chloggen/wel-supress-rendering-info.yaml @@ -0,0 +1,29 @@ +# Use this changelog template to create an entry for release notes. + +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: 'enhancement' + +# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver) +component: windowseventlogreceiver + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Add 'suppress_rendering_info' option. + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [34720] + +# (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: | + When this flag is enabled, the receiver will not attempt to resolve rendering info. This can improve performance + but comes at a cost of losing some details in the event log. + +# 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: [] diff --git a/.chloggen/wel-supress-rendering-info2.yaml b/.chloggen/wel-supress-rendering-info2.yaml new file mode 100644 index 000000000000..466f181d809e --- /dev/null +++ b/.chloggen/wel-supress-rendering-info2.yaml @@ -0,0 +1,33 @@ +# 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. filelogreceiver) +component: windowseventlogreceiver + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: The 'raw' flag no longer suppresses rendering info. + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [34720] + +# (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: | + Previously, this flag controlled two behaviors simultaneously: + 1. Whether or not the body of the log record was an XML string or structured object. + 2. Whether or not rendering info was resolved. + A separate 'suppress_rendering_info' option now controls rendering info resolution. + This is considered a breaking change because users setting only the 'raw' flag without also setting the + new 'suppress_rendering_info' flag may see a performance decrease along with more detailed events. + +# 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: [] diff --git a/.chloggen/wel-supress-rendering-info3.yaml b/.chloggen/wel-supress-rendering-info3.yaml new file mode 100644 index 000000000000..943f702a806f --- /dev/null +++ b/.chloggen/wel-supress-rendering-info3.yaml @@ -0,0 +1,27 @@ +# Use this changelog template to create an entry for release notes. + +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: 'enhancement' + +# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver) +component: windowseventlogreceiver + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Move artificial "remote_server" field to 'attributes["server.address"]'. + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [34720] + +# (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: + +# 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: [] diff --git a/pkg/stanza/docs/operators/windows_eventlog_input.md b/pkg/stanza/docs/operators/windows_eventlog_input.md index 94e791289520..792fdbc11404 100644 --- a/pkg/stanza/docs/operators/windows_eventlog_input.md +++ b/pkg/stanza/docs/operators/windows_eventlog_input.md @@ -12,6 +12,8 @@ The `windows_eventlog_input` operator reads logs from the windows event log API. | `max_reads` | 100 | The maximum number of bodies read into memory, before beginning a new batch. | | `start_at` | `end` | On first startup, where to start reading logs from the API. Options are `beginning` or `end`. | | `poll_interval` | 1s | The interval at which the channel is checked for new log entries. This check begins again after all new bodies have been read. | +| `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. | +| `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. | | `attributes` | {} | A map of `key: value` pairs to add to the entry's attributes. | | `resource` | {} | A map of `key: value` pairs to add to the entry's resource. | diff --git a/pkg/stanza/operator/input/windows/config_all.go b/pkg/stanza/operator/input/windows/config_all.go index a40ade8b6870..1952edf5c27f 100644 --- a/pkg/stanza/operator/input/windows/config_all.go +++ b/pkg/stanza/operator/input/windows/config_all.go @@ -28,14 +28,15 @@ func NewConfigWithID(operatorID string) *Config { // Config is the configuration of a windows event log operator. type Config struct { - helper.InputConfig `mapstructure:",squash"` - Channel string `mapstructure:"channel"` - MaxReads int `mapstructure:"max_reads,omitempty"` - StartAt string `mapstructure:"start_at,omitempty"` - PollInterval time.Duration `mapstructure:"poll_interval,omitempty"` - Raw bool `mapstructure:"raw,omitempty"` - ExcludeProviders []string `mapstructure:"exclude_providers,omitempty"` - Remote RemoteConfig `mapstructure:"remote,omitempty"` + helper.InputConfig `mapstructure:",squash"` + Channel string `mapstructure:"channel"` + MaxReads int `mapstructure:"max_reads,omitempty"` + StartAt string `mapstructure:"start_at,omitempty"` + PollInterval time.Duration `mapstructure:"poll_interval,omitempty"` + Raw bool `mapstructure:"raw,omitempty"` + SuppressRenderingInfo bool `mapstructure:"suppress_rendering_info,omitempty"` + ExcludeProviders []string `mapstructure:"exclude_providers,omitempty"` + Remote RemoteConfig `mapstructure:"remote,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 8b33bac9c5e1..72c5ec48f820 100644 --- a/pkg/stanza/operator/input/windows/config_windows.go +++ b/pkg/stanza/operator/input/windows/config_windows.go @@ -49,10 +49,24 @@ func (c *Config) Build(set component.TelemetrySettings) (operator.Operator, erro startAt: c.StartAt, pollInterval: c.PollInterval, raw: c.Raw, - excludeProviders: c.ExcludeProviders, + excludeProviders: excludeProvidersSet(c.ExcludeProviders), remote: c.Remote, } input.startRemoteSession = input.defaultStartRemoteSession + if c.SuppressRenderingInfo { + input.processEvent = input.processEventWithoutRenderingInfo + } else { + input.processEvent = input.processEventWithRenderingInfo + } + return input, nil } + +func excludeProvidersSet(providers []string) map[string]struct{} { + set := make(map[string]struct{}, len(providers)) + for _, provider := range providers { + set[provider] = struct{}{} + } + return set +} diff --git a/pkg/stanza/operator/input/windows/event.go b/pkg/stanza/operator/input/windows/event.go index f833fb8803fd..c55703648d25 100644 --- a/pkg/stanza/operator/input/windows/event.go +++ b/pkg/stanza/operator/input/windows/event.go @@ -12,9 +12,6 @@ import ( "unsafe" ) -// errUnknownNextFrame is an error returned when a systemcall indicates the next frame is 0 bytes. -var errUnknownNextFrame = errors.New("the buffer size needed by the next frame of a render syscall was 0, unable to determine size of next frame") - // systemPropertiesRenderContext stores a custom rendering context to get only the event properties. var systemPropertiesRenderContext = uintptr(0) var systemPropertiesRenderContextErr error @@ -72,54 +69,54 @@ func utf16PtrToString(s *uint16) string { return string(utf16.Decode(slice)) } +// NewEvent will create a new event from an event handle. +func NewEvent(handle uintptr) Event { + return Event{ + handle: handle, + } +} + // RenderSimple will render the event as EventXML without formatted info. -func (e *Event) RenderSimple(buffer Buffer) (EventXML, error) { +func (e *Event) RenderSimple(buffer Buffer) (*EventXML, error) { if e.handle == 0 { - return EventXML{}, fmt.Errorf("event handle does not exist") + return nil, fmt.Errorf("event handle does not exist") } bufferUsed, err := evtRender(0, e.handle, EvtRenderEventXML, buffer.SizeBytes(), buffer.FirstByte()) - if errors.Is(err, ErrorInsufficientBuffer) { - buffer.UpdateSizeBytes(*bufferUsed) - return e.RenderSimple(buffer) - } - if err != nil { - return EventXML{}, fmt.Errorf("syscall to 'EvtRender' failed: %w", err) + if errors.Is(err, ErrorInsufficientBuffer) { + buffer.UpdateSizeBytes(*bufferUsed) + return e.RenderSimple(buffer) + } + return nil, fmt.Errorf("syscall to 'EvtRender' failed: %w", err) } bytes, err := buffer.ReadBytes(*bufferUsed) if err != nil { - return EventXML{}, fmt.Errorf("failed to read bytes from buffer: %w", err) + return nil, fmt.Errorf("failed to read bytes from buffer: %w", err) } return unmarshalEventXML(bytes) } -// RenderFormatted will render the event as EventXML with formatted info. -func (e *Event) RenderFormatted(buffer Buffer, publisher Publisher) (EventXML, error) { +// RenderDeep will render the event as EventXML with all available formatted info. +func (e *Event) RenderDeep(buffer Buffer, publisher Publisher) (*EventXML, error) { if e.handle == 0 { - return EventXML{}, fmt.Errorf("event handle does not exist") + return nil, fmt.Errorf("event handle does not exist") } bufferUsed, err := evtFormatMessage(publisher.handle, e.handle, 0, 0, 0, EvtFormatMessageXML, buffer.SizeWide(), buffer.FirstByte()) - if errors.Is(err, ErrorInsufficientBuffer) { - // If the bufferUsed is 0 return an error as we don't want to make a recursive call with no buffer - if *bufferUsed == 0 { - return EventXML{}, errUnknownNextFrame - } - - buffer.UpdateSizeWide(*bufferUsed) - return e.RenderFormatted(buffer, publisher) - } - if err != nil { - return EventXML{}, fmt.Errorf("syscall to 'EvtFormatMessage' failed: %w", err) + if errors.Is(err, ErrorInsufficientBuffer) { + buffer.UpdateSizeWide(*bufferUsed) + return e.RenderDeep(buffer, publisher) + } + return nil, fmt.Errorf("syscall to 'EvtFormatMessage' failed: %w", err) } bytes, err := buffer.ReadWideChars(*bufferUsed) if err != nil { - return EventXML{}, fmt.Errorf("failed to read bytes from buffer: %w", err) + return nil, fmt.Errorf("failed to read bytes from buffer: %w", err) } return unmarshalEventXML(bytes) @@ -138,10 +135,3 @@ func (e *Event) Close() error { e.handle = 0 return nil } - -// NewEvent will create a new event from an event handle. -func NewEvent(handle uintptr) Event { - return Event{ - handle: handle, - } -} diff --git a/pkg/stanza/operator/input/windows/input.go b/pkg/stanza/operator/input/windows/input.go index a1f59195a18a..768d5fe40b15 100644 --- a/pkg/stanza/operator/input/windows/input.go +++ b/pkg/stanza/operator/input/windows/input.go @@ -29,7 +29,7 @@ type Input struct { maxReads int startAt string raw bool - excludeProviders []string + excludeProviders map[string]struct{} pollInterval time.Duration persister operator.Persister publisherCache publisherCache @@ -39,6 +39,7 @@ type Input struct { remote RemoteConfig remoteSessionHandle windows.Handle startRemoteSession func() error + processEvent func(context.Context, Event) } // newInput creates a new Input operator. @@ -240,90 +241,93 @@ func (i *Input) read(ctx context.Context) int { return len(events) } -// processEvent will process and send an event retrieved from windows event log. -func (i *Input) processEvent(ctx context.Context, event Event) { - var providerName string // The provider name is only retrieved if needed. - if !i.raw || len(i.excludeProviders) > 0 { - var err error - providerName, err = event.GetPublisherName(i.buffer) - if err != nil { - i.Logger().Error("Failed to get provider name", zap.Error(err)) - return - } +func (i *Input) getPublisherName(event Event) (name string, excluded bool) { + providerName, err := event.GetPublisherName(i.buffer) + if err != nil { + i.Logger().Error("Failed to get provider name", zap.Error(err)) + return "", true } - - if len(i.excludeProviders) > 0 { - for _, excludeProvider := range i.excludeProviders { - if providerName == excludeProvider { - return - } - } + if _, exclude := i.excludeProviders[providerName]; exclude { + return "", true } - if i.raw { - rawEvent, err := event.RenderSimple(i.buffer) - if err != nil { - i.Logger().Error("Failed to render raw event", zap.Error(err)) - return - } + return providerName, false +} - i.sendEventRaw(ctx, rawEvent) +func (i *Input) renderSimpleAndSend(ctx context.Context, event Event) { + simpleEvent, err := event.RenderSimple(i.buffer) + if err != nil { + i.Logger().Error("Failed to render simple event", zap.Error(err)) return } + i.sendEvent(ctx, simpleEvent) +} - publisher, openPublisherErr := i.publisherCache.get(providerName) - if openPublisherErr != nil { - // This happens only the first time the code fails to open the publisher. - i.Logger().Warn( - "Failed to open event source, respective log entries cannot be formatted", - zap.String("provider", providerName), zap.Error(openPublisherErr)) +func (i *Input) renderDeepAndSend(ctx context.Context, event Event, publisher Publisher) { + deepEvent, err := event.RenderDeep(i.buffer, publisher) + if err == nil { + i.sendEvent(ctx, deepEvent) + return } + i.Logger().Error("Failed to render formatted event", zap.Error(err)) + i.renderSimpleAndSend(ctx, event) +} - if publisher.Valid() { - formattedEvent, err := event.RenderFormatted(i.buffer, publisher) - if err == nil { - i.sendEvent(ctx, formattedEvent) - return - } +// processEvent will process and send an event retrieved from windows event log. +func (i *Input) processEventWithoutRenderingInfo(ctx context.Context, event Event) { + if len(i.excludeProviders) == 0 { + i.renderSimpleAndSend(ctx, event) + return + } + if _, exclude := i.getPublisherName(event); exclude { + return + } + i.renderSimpleAndSend(ctx, event) +} - i.Logger().Error("Failed to render formatted event", zap.Error(err)) +func (i *Input) processEventWithRenderingInfo(ctx context.Context, event Event) { + providerName, exclude := i.getPublisherName(event) + if exclude { + return } - // Falling back to simple event (non-formatted). - simpleEvent, err := event.RenderSimple(i.buffer) + publisher, err := i.publisherCache.get(providerName) if err != nil { - i.Logger().Error("Failed to render simple event", zap.Error(err)) + i.Logger().Warn( + "Failed to open event source, respective log entries cannot be formatted", + zap.String("provider", providerName), zap.Error(err)) + i.renderSimpleAndSend(ctx, event) return } - i.sendEvent(ctx, simpleEvent) + if publisher.Valid() { + i.renderDeepAndSend(ctx, event, publisher) + return + } + i.renderSimpleAndSend(ctx, event) } // sendEvent will send EventXML as an entry to the operator's output. -func (i *Input) sendEvent(ctx context.Context, eventXML EventXML) { - body := eventXML.parseBody() - entry, err := i.NewEntry(body) +func (i *Input) sendEvent(ctx context.Context, eventXML *EventXML) { + var body any = eventXML.Original + if !i.raw { + body = formattedBody(eventXML) + } + + e, err := i.NewEntry(body) if err != nil { i.Logger().Error("Failed to create entry", zap.Error(err)) return } - entry.Timestamp = eventXML.parseTimestamp() - entry.Severity = eventXML.parseRenderedSeverity() - _ = i.Write(ctx, entry) -} + e.Timestamp = parseTimestamp(eventXML.TimeCreated.SystemTime) + e.Severity = parseSeverity(eventXML.RenderedLevel, eventXML.Level) -// sendEventRaw will send EventRaw as an entry to the operator's output. -func (i *Input) sendEventRaw(ctx context.Context, eventRaw EventXML) { - entry, err := i.NewEntry(eventRaw.Original) - if err != nil { - i.Logger().Error("Failed to create entry", zap.Error(err)) - return + if i.remote.Server != "" { + e.Attributes["server.address"] = i.remote.Server } - entry.Timestamp = eventRaw.parseTimestamp() - entry.Severity = eventRaw.parseRenderedSeverity() - _ = i.Write(ctx, entry) + _ = i.Write(ctx, e) } // getBookmarkXML will get the bookmark xml from the offsets database. diff --git a/pkg/stanza/operator/input/windows/xml.go b/pkg/stanza/operator/input/windows/xml.go index 1ef1cccfe820..18cfc65a0ccb 100644 --- a/pkg/stanza/operator/input/windows/xml.go +++ b/pkg/stanza/operator/input/windows/xml.go @@ -32,22 +32,32 @@ type EventXML struct { Security *Security `xml:"System>Security"` Execution *Execution `xml:"System>Execution"` EventData EventData `xml:"EventData"` - RemoteServer string `xml:"RemoteServer,omitempty"` } // parseTimestamp will parse the timestamp of the event. -func (e *EventXML) parseTimestamp() time.Time { - if timestamp, err := time.Parse(time.RFC3339Nano, e.TimeCreated.SystemTime); err == nil { +func parseTimestamp(ts string) time.Time { + if timestamp, err := time.Parse(time.RFC3339Nano, ts); err == nil { return timestamp } return time.Now() } // parseRenderedSeverity will parse the severity of the event. -func (e *EventXML) parseRenderedSeverity() entry.Severity { - switch e.RenderedLevel { +func parseSeverity(renderedLevel, level string) entry.Severity { + switch renderedLevel { case "": - return e.parseSeverity() + switch level { + case "1": + return entry.Fatal + case "2": + return entry.Error + case "3": + return entry.Warn + case "4": + return entry.Info + default: + return entry.Default + } case "Critical": return entry.Fatal case "Error": @@ -61,25 +71,9 @@ func (e *EventXML) parseRenderedSeverity() entry.Severity { } } -// parseSeverity will parse the severity of the event when RenderingInfo is not populated -func (e *EventXML) parseSeverity() entry.Severity { - switch e.Level { - case "1": - return entry.Fatal - case "2": - return entry.Error - case "3": - return entry.Warn - case "4": - return entry.Info - default: - return entry.Default - } -} - -// parseBody will parse a body from the event. -func (e *EventXML) parseBody() map[string]any { - message, details := e.parseMessage() +// formattedBody will parse a body from the event. +func formattedBody(e *EventXML) map[string]any { + message, details := parseMessage(e.Channel, e.Message) level := e.RenderedLevel if level == "" { @@ -123,10 +117,6 @@ func (e *EventXML) parseBody() map[string]any { "event_data": parseEventData(e.EventData), } - if e.RemoteServer != "" { - body["remote_server"] = e.RemoteServer - } - if len(details) > 0 { body["details"] = details } @@ -145,12 +135,12 @@ func (e *EventXML) parseBody() map[string]any { } // parseMessage will attempt to parse a message into a message and details -func (e *EventXML) parseMessage() (string, map[string]any) { - switch e.Channel { +func parseMessage(channel, message string) (string, map[string]any) { + switch channel { case "Security": - return parseSecurity(e.Message) + return parseSecurity(message) default: - return e.Message, nil + return message, nil } } @@ -261,11 +251,11 @@ func (e Execution) asMap() map[string]any { } // unmarshalEventXML will unmarshal EventXML from xml bytes. -func unmarshalEventXML(bytes []byte) (EventXML, error) { +func unmarshalEventXML(bytes []byte) (*EventXML, error) { var eventXML EventXML if err := xml.Unmarshal(bytes, &eventXML); err != nil { - return EventXML{}, fmt.Errorf("failed to unmarshal xml bytes into event: %w (%s)", err, string(bytes)) + return nil, fmt.Errorf("failed to unmarshal xml bytes into event: %w (%s)", err, string(bytes)) } eventXML.Original = string(bytes) - return eventXML, nil + return &eventXML, nil } diff --git a/pkg/stanza/operator/input/windows/xml_test.go b/pkg/stanza/operator/input/windows/xml_test.go index 05bb47dd49bb..41991daac2cd 100644 --- a/pkg/stanza/operator/input/windows/xml_test.go +++ b/pkg/stanza/operator/input/windows/xml_test.go @@ -15,53 +15,33 @@ import ( ) func TestParseValidTimestamp(t *testing.T) { - xml := EventXML{ - TimeCreated: TimeCreated{ - SystemTime: "2020-07-30T01:01:01.123456789Z", - }, - } - timestamp := xml.parseTimestamp() + timestamp := parseTimestamp("2020-07-30T01:01:01.123456789Z") expected, _ := time.Parse(time.RFC3339Nano, "2020-07-30T01:01:01.123456789Z") require.Equal(t, expected, timestamp) } func TestParseInvalidTimestamp(t *testing.T) { - xml := EventXML{ - TimeCreated: TimeCreated{ - SystemTime: "invalid", - }, - } - timestamp := xml.parseTimestamp() + timestamp := parseTimestamp("invalid") require.Equal(t, time.Now().Year(), timestamp.Year()) require.Equal(t, time.Now().Month(), timestamp.Month()) require.Equal(t, time.Now().Day(), timestamp.Day()) } func TestParseSeverity(t *testing.T) { - xmlRenderedCritical := EventXML{RenderedLevel: "Critical"} - xmlRenderedError := EventXML{RenderedLevel: "Error"} - xmlRenderedWarning := EventXML{RenderedLevel: "Warning"} - xmlRenderedInformation := EventXML{RenderedLevel: "Information"} - xmlRenderedUnknown := EventXML{RenderedLevel: "Unknown"} - xmlCritical := EventXML{Level: "1"} - xmlError := EventXML{Level: "2"} - xmlWarning := EventXML{Level: "3"} - xmlInformation := EventXML{Level: "4"} - xmlUnknown := EventXML{Level: "0"} - require.Equal(t, entry.Fatal, xmlRenderedCritical.parseRenderedSeverity()) - require.Equal(t, entry.Error, xmlRenderedError.parseRenderedSeverity()) - require.Equal(t, entry.Warn, xmlRenderedWarning.parseRenderedSeverity()) - require.Equal(t, entry.Info, xmlRenderedInformation.parseRenderedSeverity()) - require.Equal(t, entry.Default, xmlRenderedUnknown.parseRenderedSeverity()) - require.Equal(t, entry.Fatal, xmlCritical.parseRenderedSeverity()) - require.Equal(t, entry.Error, xmlError.parseRenderedSeverity()) - require.Equal(t, entry.Warn, xmlWarning.parseRenderedSeverity()) - require.Equal(t, entry.Info, xmlInformation.parseRenderedSeverity()) - require.Equal(t, entry.Default, xmlUnknown.parseRenderedSeverity()) + require.Equal(t, entry.Fatal, parseSeverity("Critical", "")) + require.Equal(t, entry.Error, parseSeverity("Error", "")) + require.Equal(t, entry.Warn, parseSeverity("Warning", "")) + require.Equal(t, entry.Info, parseSeverity("Information", "")) + require.Equal(t, entry.Default, parseSeverity("Unknown", "")) + require.Equal(t, entry.Fatal, parseSeverity("", "1")) + require.Equal(t, entry.Error, parseSeverity("", "2")) + require.Equal(t, entry.Warn, parseSeverity("", "3")) + require.Equal(t, entry.Info, parseSeverity("", "4")) + require.Equal(t, entry.Default, parseSeverity("", "0")) } func TestParseBody(t *testing.T) { - xml := EventXML{ + xml := &EventXML{ EventID: EventID{ ID: 1, Qualifiers: 2, @@ -118,11 +98,11 @@ func TestParseBody(t *testing.T) { }, } - require.Equal(t, expected, xml.parseBody()) + require.Equal(t, expected, formattedBody(xml)) } func TestParseBodySecurityExecution(t *testing.T) { - xml := EventXML{ + xml := &EventXML{ EventID: EventID{ ID: 1, Qualifiers: 2, @@ -193,7 +173,7 @@ func TestParseBodySecurityExecution(t *testing.T) { }, } - require.Equal(t, expected, xml.parseBody()) + require.Equal(t, expected, formattedBody(xml)) } func TestParseBodyFullExecution(t *testing.T) { @@ -203,7 +183,7 @@ func TestParseBodyFullExecution(t *testing.T) { userTime := uint(100) processorTime := uint(200) - xml := EventXML{ + xml := &EventXML{ EventID: EventID{ ID: 1, Qualifiers: 2, @@ -284,11 +264,11 @@ func TestParseBodyFullExecution(t *testing.T) { }, } - require.Equal(t, expected, xml.parseBody()) + require.Equal(t, expected, formattedBody(xml)) } func TestParseNoRendered(t *testing.T) { - xml := EventXML{ + xml := &EventXML{ EventID: EventID{ ID: 1, Qualifiers: 2, @@ -341,11 +321,11 @@ func TestParseNoRendered(t *testing.T) { }, } - require.Equal(t, expected, xml.parseBody()) + require.Equal(t, expected, formattedBody(xml)) } func TestParseBodySecurity(t *testing.T) { - xml := EventXML{ + xml := &EventXML{ EventID: EventID{ ID: 1, Qualifiers: 2, @@ -402,11 +382,11 @@ func TestParseBodySecurity(t *testing.T) { }, } - require.Equal(t, expected, xml.parseBody()) + require.Equal(t, expected, formattedBody(xml)) } func TestParseEventData(t *testing.T) { - xmlMap := EventXML{ + xmlMap := &EventXML{ EventData: EventData{ Name: "EVENT_DATA", Data: []Data{{Name: "name", Value: "value"}}, @@ -414,7 +394,7 @@ func TestParseEventData(t *testing.T) { }, } - parsed := xmlMap.parseBody() + parsed := formattedBody(xmlMap) expectedMap := map[string]any{ "name": "EVENT_DATA", "data": []any{ @@ -424,13 +404,13 @@ func TestParseEventData(t *testing.T) { } require.Equal(t, expectedMap, parsed["event_data"]) - xmlMixed := EventXML{ + xmlMixed := &EventXML{ EventData: EventData{ Data: []Data{{Name: "name", Value: "value"}, {Value: "no_name"}}, }, } - parsed = xmlMixed.parseBody() + parsed = formattedBody(xmlMixed) expectedSlice := map[string]any{ "data": []any{ map[string]any{"name": "value"}, @@ -451,7 +431,7 @@ func TestUnmarshalWithEventData(t *testing.T) { event, err := unmarshalEventXML(data) require.NoError(t, err) - xml := EventXML{ + xml := &EventXML{ EventID: EventID{ ID: 16384, Qualifiers: 16384, @@ -492,7 +472,7 @@ func TestUnmarshalWithAnonymousEventDataEntries(t *testing.T) { event, err := unmarshalEventXML(data) require.NoError(t, err) - xml := EventXML{ + xml := &EventXML{ EventID: EventID{ ID: 8194, Qualifiers: 0, @@ -530,7 +510,7 @@ func TestUnmarshalWithUserData(t *testing.T) { event, err := unmarshalEventXML(data) require.NoError(t, err) - xml := EventXML{ + xml := &EventXML{ EventID: EventID{ ID: 1102, }, @@ -561,126 +541,3 @@ func TestUnmarshalWithUserData(t *testing.T) { require.Equal(t, xml, event) } - -func TestParseBodyRemoteServer(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: "1st_name", Value: "value"}, {Name: "2nd_name", Value: "another_value"}}}, - RenderedLevel: "rendered_level", - RenderedTask: "rendered_task", - RenderedOpcode: "rendered_opcode", - RenderedKeywords: []string{"RenderedKeywords"}, - RemoteServer: "remote_server", - } - - expected := map[string]any{ - "event_id": map[string]any{ - "id": uint32(1), - "qualifiers": uint16(2), - }, - "provider": map[string]any{ - "name": "provider", - "guid": "guid", - "event_source": "event source", - }, - "system_time": "2020-07-30T01:01:01.123456789Z", - "computer": "computer", - "channel": "application", - "record_id": uint64(1), - "level": "rendered_level", - "message": "message", - "task": "rendered_task", - "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"}, - }, - }, - "remote_server": "remote_server", - } - - require.Equal(t, expected, xml.parseBody()) -} - -// Additional test cases to ensure comprehensive coverage - -func TestParseBodyNoRemoteServer(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: "1st_name", Value: "value"}, {Name: "2nd_name", Value: "another_value"}}}, - RenderedLevel: "rendered_level", - RenderedTask: "rendered_task", - RenderedOpcode: "rendered_opcode", - RenderedKeywords: []string{"RenderedKeywords"}, - RemoteServer: "", - } - - expected := map[string]any{ - "event_id": map[string]any{ - "id": uint32(1), - "qualifiers": uint16(2), - }, - "provider": map[string]any{ - "name": "provider", - "guid": "guid", - "event_source": "event source", - }, - "system_time": "2020-07-30T01:01:01.123456789Z", - "computer": "computer", - "channel": "application", - "record_id": uint64(1), - "level": "rendered_level", - "message": "message", - "task": "rendered_task", - "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"}, - }, - }, - } - - require.Equal(t, expected, xml.parseBody()) -} diff --git a/receiver/windowseventlogreceiver/README.md b/receiver/windowseventlogreceiver/README.md index 83314ec4a4bf..89da16e957cb 100644 --- a/receiver/windowseventlogreceiver/README.md +++ b/receiver/windowseventlogreceiver/README.md @@ -26,14 +26,15 @@ Tails and parses logs from windows event log API using the [opentelemetry-log-co | `attributes` | {} | A map of `key: value` pairs to add to the entry's attributes. | | `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 true, the windows events are not processed and sent as XML. If used in combination with `exclude_providers`, each event will be processed in order to determine its provider name. | +| `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. | +| `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. | | `storage` | none | The ID of a storage extension to be used to store bookmarks. Bookmarks allow the receiver to pick up where it left off in the case of a collector restart. If no storage extension is used, the receiver will manage bookmarks in memory only. | | `retry_on_failure.enabled` | `false` | If `true`, the receiver will pause reading a file and attempt to resend the current batch of logs if it encounters an error from downstream components. | | `retry_on_failure.initial_interval` | `1 second` | Time to wait after the first failure before retrying. | | `retry_on_failure.max_interval` | `30 seconds` | Upper bound on retry backoff interval. Once this value is reached the delay between consecutive retries will remain constant at the specified value. | | `retry_on_failure.max_elapsed_time` | `5 minutes` | Maximum amount of time (including retries) spent trying to send a logs batch to a downstream consumer. Once this value is reached, the data is discarded. Retrying never stops if set to `0`. | -| remote | object | Remote configuration for connecting to a remote machine to collect logs. Includes server (the address of the remote server), with username, password, and optional domain. | +| `remote` | object | Remote configuration for connecting to a remote machine to collect logs. Includes server (the address of the remote server), with username, password, and optional domain. | ### Operators