Skip to content

Commit 0245843

Browse files
[receiver/windowseventlogreceiver]: add IncludeLogRecordOriginal to add log.record.original to attributes (open-telemetry#40367)
#### Description - Create an opt-in boolean option in the config called `include_log_record_original`. - When true, the receive puts the log's raw XML value into a [general log identification attribute](https://opentelemetry.io/docs/specs/semconv/general/logs/): log.record.original. #### Link to tracking issue open-telemetry#40365 #### Documentation - Update README.md
1 parent 1de3ee6 commit 0245843

File tree

7 files changed

+211
-42
lines changed

7 files changed

+211
-42
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: enhancement
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
7+
component: windowseventlogreceiver
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: Add a boolean option to include the `log.record.original` attribute of each event record.
11+
12+
# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
13+
issues: [40365]
14+
15+
# (Optional) One or more lines of additional information to render under the primary note.
16+
# These lines will be padded with 2 spaces and then inserted directly into the document.
17+
# Use pipe (|) for multiline entries.
18+
subtext:
19+
20+
# If your change doesn't affect end users or the exported elements of any package,
21+
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
22+
# Optional: The change log or logs in which this entry should be included.
23+
# e.g. '[user]' or '[user, api]'
24+
# Include 'user' if the change is relevant to end users.
25+
# Include 'api' if there is a change to a library API.
26+
# Default: '[user]'
27+
change_logs: [user]

pkg/stanza/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ require (
2828
go.opentelemetry.io/collector/receiver v1.33.1-0.20250605211604-c9aaed834963
2929
go.opentelemetry.io/collector/receiver/receiverhelper v0.127.1-0.20250605211604-c9aaed834963
3030
go.opentelemetry.io/collector/receiver/receivertest v0.127.1-0.20250605211604-c9aaed834963
31+
go.opentelemetry.io/otel v1.36.0
3132
go.opentelemetry.io/otel/metric v1.36.0
3233
go.opentelemetry.io/otel/sdk/metric v1.36.0
3334
go.opentelemetry.io/otel/trace v1.36.0
@@ -75,7 +76,6 @@ require (
7576
go.opentelemetry.io/collector/pipeline v0.127.1-0.20250605211604-c9aaed834963 // indirect
7677
go.opentelemetry.io/collector/receiver/xreceiver v0.127.1-0.20250605211604-c9aaed834963 // indirect
7778
go.opentelemetry.io/contrib/bridges/otelzap v0.11.0 // indirect
78-
go.opentelemetry.io/otel v1.36.0 // indirect
7979
go.opentelemetry.io/otel/log v0.12.2 // indirect
8080
go.opentelemetry.io/otel/sdk v1.36.0 // indirect
8181
golang.org/x/crypto v0.37.0 // indirect

pkg/stanza/operator/input/windows/config_all.go

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,17 @@ func NewConfigWithID(operatorID string) *Config {
2828

2929
// Config is the configuration of a windows event log operator.
3030
type Config struct {
31-
helper.InputConfig `mapstructure:",squash"`
32-
Channel string `mapstructure:"channel"`
33-
MaxReads int `mapstructure:"max_reads,omitempty"`
34-
StartAt string `mapstructure:"start_at,omitempty"`
35-
PollInterval time.Duration `mapstructure:"poll_interval,omitempty"`
36-
Raw bool `mapstructure:"raw,omitempty"`
37-
SuppressRenderingInfo bool `mapstructure:"suppress_rendering_info,omitempty"`
38-
ExcludeProviders []string `mapstructure:"exclude_providers,omitempty"`
39-
Remote RemoteConfig `mapstructure:"remote,omitempty"`
40-
Query *string `mapstructure:"query,omitempty"`
31+
helper.InputConfig `mapstructure:",squash"`
32+
Channel string `mapstructure:"channel"`
33+
MaxReads int `mapstructure:"max_reads,omitempty"`
34+
StartAt string `mapstructure:"start_at,omitempty"`
35+
PollInterval time.Duration `mapstructure:"poll_interval,omitempty"`
36+
Raw bool `mapstructure:"raw,omitempty"`
37+
IncludeLogRecordOriginal bool `mapstructure:"include_log_record_original,omitempty"`
38+
SuppressRenderingInfo bool `mapstructure:"suppress_rendering_info,omitempty"`
39+
ExcludeProviders []string `mapstructure:"exclude_providers,omitempty"`
40+
Remote RemoteConfig `mapstructure:"remote,omitempty"`
41+
Query *string `mapstructure:"query,omitempty"`
4142
}
4243

4344
// RemoteConfig is the configuration for a remote server.

pkg/stanza/operator/input/windows/config_windows.go

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,18 @@ func (c *Config) Build(set component.TelemetrySettings) (operator.Operator, erro
4646
}
4747

4848
input := &Input{
49-
InputOperator: inputOperator,
50-
buffer: NewBuffer(),
51-
channel: c.Channel,
52-
maxReads: c.MaxReads,
53-
currentMaxReads: c.MaxReads,
54-
startAt: c.StartAt,
55-
pollInterval: c.PollInterval,
56-
raw: c.Raw,
57-
excludeProviders: excludeProvidersSet(c.ExcludeProviders),
58-
remote: c.Remote,
59-
query: c.Query,
49+
InputOperator: inputOperator,
50+
buffer: NewBuffer(),
51+
channel: c.Channel,
52+
maxReads: c.MaxReads,
53+
currentMaxReads: c.MaxReads,
54+
startAt: c.StartAt,
55+
pollInterval: c.PollInterval,
56+
raw: c.Raw,
57+
includeLogRecordOriginal: c.IncludeLogRecordOriginal,
58+
excludeProviders: excludeProvidersSet(c.ExcludeProviders),
59+
remote: c.Remote,
60+
query: c.Query,
6061
}
6162
input.startRemoteSession = input.defaultStartRemoteSession
6263

pkg/stanza/operator/input/windows/input.go

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"time"
1414

1515
"go.opentelemetry.io/collector/component"
16+
semconv "go.opentelemetry.io/otel/semconv/v1.27.0"
1617
"go.uber.org/multierr"
1718
"go.uber.org/zap"
1819
"golang.org/x/sys/windows"
@@ -24,25 +25,26 @@ import (
2425
// Input is an operator that creates entries using the windows event log api.
2526
type Input struct {
2627
helper.InputOperator
27-
bookmark Bookmark
28-
buffer *Buffer
29-
channel string
30-
query *string
31-
maxReads int
32-
currentMaxReads int
33-
startAt string
34-
raw bool
35-
excludeProviders map[string]struct{}
36-
pollInterval time.Duration
37-
persister operator.Persister
38-
publisherCache publisherCache
39-
cancel context.CancelFunc
40-
wg sync.WaitGroup
41-
subscription Subscription
42-
remote RemoteConfig
43-
remoteSessionHandle windows.Handle
44-
startRemoteSession func() error
45-
processEvent func(context.Context, Event) error
28+
bookmark Bookmark
29+
buffer *Buffer
30+
channel string
31+
query *string
32+
maxReads int
33+
currentMaxReads int
34+
startAt string
35+
raw bool
36+
includeLogRecordOriginal bool
37+
excludeProviders map[string]struct{}
38+
pollInterval time.Duration
39+
persister operator.Persister
40+
publisherCache publisherCache
41+
cancel context.CancelFunc
42+
wg sync.WaitGroup
43+
subscription Subscription
44+
remote RemoteConfig
45+
remoteSessionHandle windows.Handle
46+
startRemoteSession func() error
47+
processEvent func(context.Context, Event) error
4648
}
4749

4850
// newInput creates a new Input operator.
@@ -327,7 +329,11 @@ func (i *Input) sendEvent(ctx context.Context, eventXML *EventXML) error {
327329
e.Severity = parseSeverity(eventXML.RenderedLevel, eventXML.Level)
328330

329331
if i.remote.Server != "" {
330-
e.Attributes["server.address"] = i.remote.Server
332+
e.AddAttribute("server.address", i.remote.Server)
333+
}
334+
335+
if i.includeLogRecordOriginal {
336+
e.AddAttribute(string(semconv.LogRecordOriginalKey), eventXML.Original)
331337
}
332338

333339
return i.Write(ctx, e)

pkg/stanza/operator/input/windows/input_test.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import (
1818
"go.uber.org/zap/zaptest/observer"
1919
"golang.org/x/sys/windows"
2020

21+
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza/entry"
22+
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza/operator"
2123
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza/testutil"
2224
)
2325

@@ -211,3 +213,134 @@ func TestInputRead_RPCInvalidBound(t *testing.T) {
211213
require.Equal(t, 1, logs.Len())
212214
assert.Contains(t, logs.All()[0].Message, "Encountered RPC_S_INVALID_BOUND")
213215
}
216+
217+
// TestInputIncludeLogRecordOriginal tests that the log.record.original attribute is added when include_log_record_original is true
218+
func TestInputIncludeLogRecordOriginal(t *testing.T) {
219+
input := newTestInput()
220+
input.includeLogRecordOriginal = true
221+
input.pollInterval = time.Second
222+
input.buffer = NewBuffer() // Initialize buffer
223+
224+
// Create a mock event XML
225+
eventXML := &EventXML{
226+
Original: "<Event><System><Provider Name='TestProvider'/><EventID>1</EventID></System></Event>",
227+
TimeCreated: TimeCreated{
228+
SystemTime: "2024-01-01T00:00:00Z",
229+
},
230+
}
231+
232+
ctx := context.Background()
233+
persister := testutil.NewMockPersister("")
234+
fake := testutil.NewFakeOutput(t)
235+
input.OutputOperators = []operator.Operator{fake}
236+
237+
err := input.Start(persister)
238+
require.NoError(t, err)
239+
240+
err = input.sendEvent(ctx, eventXML)
241+
require.NoError(t, err)
242+
243+
expectedEntry := &entry.Entry{
244+
Timestamp: time.Date(2024, time.January, 1, 0, 0, 0, 0, time.UTC),
245+
Body: map[string]any{
246+
"channel": "",
247+
"computer": "",
248+
"event_data": map[string]any{},
249+
"event_id": map[string]any{
250+
"id": uint32(0),
251+
"qualifiers": uint16(0),
252+
},
253+
"keywords": []string(nil),
254+
"level": "",
255+
"message": "",
256+
"opcode": "",
257+
"provider": map[string]any{
258+
"event_source": "",
259+
"guid": "",
260+
"name": "",
261+
},
262+
"record_id": uint64(0),
263+
"system_time": "2024-01-01T00:00:00Z",
264+
"task": "",
265+
},
266+
Attributes: map[string]any{
267+
"log.record.original": eventXML.Original,
268+
},
269+
}
270+
271+
select {
272+
case actualEntry := <-fake.Received:
273+
actualEntry.ObservedTimestamp = time.Time{}
274+
assert.Equal(t, expectedEntry, actualEntry)
275+
case <-time.After(time.Second):
276+
require.FailNow(t, "Timed out waiting for entry")
277+
}
278+
279+
err = input.Stop()
280+
require.NoError(t, err)
281+
}
282+
283+
// TestInputIncludeLogRecordOriginalFalse tests that the log.record.original attribute is not added when include_log_record_original is false
284+
func TestInputIncludeLogRecordOriginalFalse(t *testing.T) {
285+
input := newTestInput()
286+
input.includeLogRecordOriginal = false
287+
input.pollInterval = time.Second
288+
input.buffer = NewBuffer() // Initialize buffer
289+
290+
// Create a mock event XML
291+
eventXML := &EventXML{
292+
Original: "<Event><System><Provider Name='TestProvider'/><EventID>1</EventID></System></Event>",
293+
TimeCreated: TimeCreated{
294+
SystemTime: "2024-01-01T00:00:00Z",
295+
},
296+
}
297+
298+
ctx := context.Background()
299+
persister := testutil.NewMockPersister("")
300+
fake := testutil.NewFakeOutput(t)
301+
input.OutputOperators = []operator.Operator{fake}
302+
303+
err := input.Start(persister)
304+
require.NoError(t, err)
305+
306+
err = input.sendEvent(ctx, eventXML)
307+
require.NoError(t, err)
308+
309+
expectedEntry := &entry.Entry{
310+
Timestamp: time.Date(2024, time.January, 1, 0, 0, 0, 0, time.UTC),
311+
Body: map[string]any{
312+
"channel": "",
313+
"computer": "",
314+
"event_data": map[string]any{},
315+
"event_id": map[string]any{
316+
"id": uint32(0),
317+
"qualifiers": uint16(0),
318+
},
319+
"keywords": []string(nil),
320+
"level": "",
321+
"message": "",
322+
"opcode": "",
323+
"provider": map[string]any{
324+
"event_source": "",
325+
"guid": "",
326+
"name": "",
327+
},
328+
"record_id": uint64(0),
329+
"system_time": "2024-01-01T00:00:00Z",
330+
"task": "",
331+
},
332+
Attributes: nil,
333+
}
334+
335+
// Verify that log.record.original attribute does not exist
336+
select {
337+
case actualEntry := <-fake.Received:
338+
actualEntry.ObservedTimestamp = time.Time{}
339+
assert.Equal(t, expectedEntry, actualEntry)
340+
case <-time.After(time.Second):
341+
require.FailNow(t, "Timed out waiting for entry")
342+
}
343+
344+
err = input.Stop()
345+
require.NoError(t, err)
346+
}

receiver/windowseventlogreceiver/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Tails and parses logs from windows event log API using the [opentelemetry-log-co
2828
| `resource` | {} | A map of `key: value` pairs to add to the entry's resource. |
2929
| `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 |
3030
| `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. |
31+
| `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).
3132
| `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. |
3233
| `exclude_providers` | [] | One or more event log providers to exclude from processing. |
3334
| `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. |

0 commit comments

Comments
 (0)