Skip to content

perf: skip unnecessary event.Clone() in processors for single-field ops#49777

Open
strawgate wants to merge 2 commits intoelastic:mainfrom
strawgate:perf/skip-unnecessary-clone
Open

perf: skip unnecessary event.Clone() in processors for single-field ops#49777
strawgate wants to merge 2 commits intoelastic:mainfrom
strawgate:perf/skip-unnecessary-clone

Conversation

@strawgate
Copy link
Copy Markdown
Contributor

@strawgate strawgate commented Mar 30, 2026

Proposed commit message

Skip unnecessary event.Clone() in 11 processors. The clone deep-copies the entire accumulated event for rollback, but single-field operations and dissect don't need it.

  • dissect: Restructure mapper() to check all target keys before writing any. No partial writes possible, so no rollback needed regardless of config.
  • rename: Skip clone for single non-overlapping field renames. New renameFieldSafe validates target path via GetValue before Delete, preventing data loss without a clone.
  • copy_fields, replace, truncate_fields, urldecode, extract_array, decode_csv_fields: Skip clone when len(fields) == 1.
  • decode_base64_field, decompress_gzip_field, append: Remove clone (always single operation).

Multi-field operations retain the clone. Follows the pattern established by the convert processor in 2019 (#11686).

Per-processor benchmarks:

Processor Δ ns/op Δ B/op Δ allocs/op
rename (1 field) -54% -46% -48%
copy_fields (1 field) -61% -48% -48%

Checklist

  • My code follows the style guidelines of this project
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • I have made corresponding change to the default configuration files
  • I have added tests that prove my fix is effective or that my feature works.
  • I have added an entry in ./changelog/fragments using the changelog tool.

Disruptive User Impact

None. All error conditions, error messages, and event mutations are identical to main. The rename change uses renameFieldSafe which detects blocked paths (scalar where map expected) before deleting the source field, producing the same error but without data loss.

How to test this PR locally

go test -bench=BenchmarkRenameSingleField -benchmem ./libbeat/processors/actions/
go test ./libbeat/processors/actions/ ./libbeat/processors/dissect/

@strawgate strawgate requested a review from a team as a code owner March 30, 2026 13:22
@strawgate strawgate requested review from faec and khushijain21 March 30, 2026 13:22
@botelastic botelastic bot added the needs_team Indicates that the issue/PR needs a Team:* label label Mar 30, 2026
@github-actions
Copy link
Copy Markdown
Contributor

🤖 GitHub comments

Just comment with:

  • run docs-build : Re-trigger the docs validation. (use unformatted text in the comment!)

@strawgate strawgate changed the title perf: skip unnecessary event.Clone() in processors for single-field o… perf: skip unnecessary event.Clone() in processors for single-field ops Mar 30, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 30, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Optimized event cloning behavior across multiple libbeat processors. When fail_on_error is enabled, processors now clone the event only when multiple field configurations exist, rather than for every fail_on_error case. Some processors removed cloning entirely. Error handling revised to guard backup restoration with nil checks. Additional changes include refactored conflict detection in the dissect processor, shifted field references from config to embedded fields in extract_array, and lowercase corrections to error message prefixes. A changelog entry documenting the enhancement was added.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • 🛠️ Update Documentation: Commit on current branch
  • 🛠️ Update Documentation: Create PR

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@libbeat/processors/actions/rename.go`:
- Around line 79-80: The code deletes the source field before ensuring the
destination write succeeds, and because renameNeedsClone can return false for
single non-overlapping fields no backup is kept when f.config.FailOnError is
true; change the logic so that when f.config.FailOnError is true you always
preserve a backup (create backup = event.Clone()) before mutating, or alter
renameField to perform PutValue(to, value) first and only remove the original
(from) after PutValue succeeds, and if PutValue fails restore the backup; update
the same pattern in the other affected blocks (the similar logic around lines
90-92 and 140-151) to use the same safe sequence (backup on FailOnError or
write-then-delete) and reference f.config.FailOnError, renameNeedsClone, backup,
renameField, PutValue, from and to when making the changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 128dd85b-820c-4316-8e09-dc37698937ae

📥 Commits

Reviewing files that changed from the base of the PR and between e92763f and d883a3d.

📒 Files selected for processing (12)
  • libbeat/processors/actions/append.go
  • libbeat/processors/actions/clone_skip_test.go
  • libbeat/processors/actions/copy_fields.go
  • libbeat/processors/actions/decode_base64_field.go
  • libbeat/processors/actions/decompress_gzip_field.go
  • libbeat/processors/actions/rename.go
  • libbeat/processors/actions/replace.go
  • libbeat/processors/actions/truncate_fields.go
  • libbeat/processors/decode_csv_fields/decode_csv_fields.go
  • libbeat/processors/dissect/processor.go
  • libbeat/processors/extract_array/extract_array.go
  • libbeat/processors/urldecode/urldecode.go
💤 Files with no reviewable changes (3)
  • libbeat/processors/actions/decompress_gzip_field.go
  • libbeat/processors/actions/decode_base64_field.go
  • libbeat/processors/actions/append.go

@strawgate strawgate added backport-skip Skip notification from the automated backport with mergify skip-changelog labels Mar 30, 2026
strawgate added a commit to strawgate/beats that referenced this pull request Mar 30, 2026
The dissect event.Clone() skip is moving to elastic#49777 which applies
the same pattern more broadly across all action processors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@elastic elastic deleted a comment from botelastic bot Mar 30, 2026
@elastic elastic deleted a comment from mergify bot Mar 30, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@libbeat/processors/actions/rename.go`:
- Line 86: The error string constructed in rename.go (variable errMsg created
via fmt.Errorf in the rename processor) uses a lowercase "failed to rename..."
which mismatches tests expecting "Failed to rename..."; update the fmt.Errorf
call that sets errMsg to use a capitalized "Failed to rename fields in
processor: %w" so the error message matches the assertions in rename_test.go (or
alternatively update the tests if you prefer changing expectations).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 87c29645-1654-4003-988e-bb655b87972c

📥 Commits

Reviewing files that changed from the base of the PR and between d883a3d and 6d5ad2d.

📒 Files selected for processing (6)
  • changelog/fragments/1774840000-skip-unnecessary-event-clone.yaml
  • libbeat/processors/actions/append.go
  • libbeat/processors/actions/copy_fields.go
  • libbeat/processors/actions/decompress_gzip_field.go
  • libbeat/processors/actions/rename.go
  • libbeat/processors/extract_array/extract_array.go
✅ Files skipped from review due to trivial changes (2)
  • changelog/fragments/1774840000-skip-unnecessary-event-clone.yaml
  • libbeat/processors/actions/append.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • libbeat/processors/actions/decompress_gzip_field.go
  • libbeat/processors/actions/copy_fields.go

@strawgate strawgate force-pushed the perf/skip-unnecessary-clone branch from 6d5ad2d to 7edb9a7 Compare March 30, 2026 13:49
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
libbeat/processors/actions/rename.go (1)

79-80: ⚠️ Potential issue | 🔴 Critical

fail_on_error can still lose data in single-field rename

Line 79 skips backup for single-field non-overlapping renames, but Lines 118-127 still do delete-before-put. If PutValue fails (e.g., blocked destination path), source data is already deleted and cannot be restored despite fail_on_error: true.

Proposed fix
 func (f *renameFields) renameField(from string, to string, event *beat.Event) error {
@@
-	// Deletion must happen first to support cases where a becomes a.b
-	err = event.Delete(from)
-	if err != nil {
-		return fmt.Errorf("could not delete key: %s,  %w", from, err)
-	}
-
-	_, err = event.PutValue(to, value)
-	if err != nil {
-		return fmt.Errorf("could not put value: %s: %v, %w", to, value, err)
-	}
+	overlap := strings.HasPrefix(to, from+".") || strings.HasPrefix(from, to+".")
+	if !overlap {
+		if _, err = event.PutValue(to, value); err != nil {
+			return fmt.Errorf("could not put value: %s: %v, %w", to, value, err)
+		}
+		if err = event.Delete(from); err != nil {
+			_, _ = event.Delete(to) // best-effort local rollback
+			return fmt.Errorf("could not delete key: %s, %w", from, err)
+		}
+		return nil
+	}
+
+	// overlap case still requires delete-first
+	err = event.Delete(from)
+	if err != nil {
+		return fmt.Errorf("could not delete key: %s,  %w", from, err)
+	}
+	_, err = event.PutValue(to, value)
+	if err != nil {
+		return fmt.Errorf("could not put value: %s: %v, %w", to, value, err)
+	}
 	return nil
 }

Also applies to: 118-127, 140-152

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libbeat/processors/actions/rename.go` around lines 79 - 80, The current logic
in rename.go only creates a backup when renameNeedsClone(f.config) is true,
which skips backups for single-field non-overlapping renames even though later
code paths (uses of PutValue and delete-before-put in the blocks around where
PutValue is called) perform a delete-before-put; to fix, ensure that when
f.config.FailOnError is true you create a backup (e.g., call event.Clone into
backup) for all rename cases that perform delete-before-put (including the
single-field path), or alternatively change the operation order in the rename
implementation so you perform PutValue (destination write) before deleting the
source, and on failure restore from the backup; update the code paths referenced
by renameNeedsClone, the backup variable, and the PutValue/delete logic (the
blocks around PutValue and the delete-before-put sections) so fail_on_error
truly preserves source data.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@libbeat/processors/actions/rename.go`:
- Around line 79-80: The current logic in rename.go only creates a backup when
renameNeedsClone(f.config) is true, which skips backups for single-field
non-overlapping renames even though later code paths (uses of PutValue and
delete-before-put in the blocks around where PutValue is called) perform a
delete-before-put; to fix, ensure that when f.config.FailOnError is true you
create a backup (e.g., call event.Clone into backup) for all rename cases that
perform delete-before-put (including the single-field path), or alternatively
change the operation order in the rename implementation so you perform PutValue
(destination write) before deleting the source, and on failure restore from the
backup; update the code paths referenced by renameNeedsClone, the backup
variable, and the PutValue/delete logic (the blocks around PutValue and the
delete-before-put sections) so fail_on_error truly preserves source data.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 175d3224-5645-436b-9b15-696f89e8eb98

📥 Commits

Reviewing files that changed from the base of the PR and between 6d5ad2d and 7edb9a7.

📒 Files selected for processing (13)
  • changelog/fragments/1774840000-skip-unnecessary-event-clone.yaml
  • libbeat/processors/actions/append.go
  • libbeat/processors/actions/clone_skip_test.go
  • libbeat/processors/actions/copy_fields.go
  • libbeat/processors/actions/decode_base64_field.go
  • libbeat/processors/actions/decompress_gzip_field.go
  • libbeat/processors/actions/rename.go
  • libbeat/processors/actions/replace.go
  • libbeat/processors/actions/truncate_fields.go
  • libbeat/processors/decode_csv_fields/decode_csv_fields.go
  • libbeat/processors/dissect/processor.go
  • libbeat/processors/extract_array/extract_array.go
  • libbeat/processors/urldecode/urldecode.go
💤 Files with no reviewable changes (1)
  • libbeat/processors/actions/decode_base64_field.go
✅ Files skipped from review due to trivial changes (2)
  • changelog/fragments/1774840000-skip-unnecessary-event-clone.yaml
  • libbeat/processors/actions/replace.go
🚧 Files skipped from review as they are similar to previous changes (6)
  • libbeat/processors/actions/truncate_fields.go
  • libbeat/processors/actions/append.go
  • libbeat/processors/extract_array/extract_array.go
  • libbeat/processors/actions/copy_fields.go
  • libbeat/processors/actions/decompress_gzip_field.go
  • libbeat/processors/dissect/processor.go

@github-actions

This comment has been minimized.

@pierrehilbert pierrehilbert added the Team:Elastic-Agent-Data-Plane Label for the Agent Data Plane team label Mar 30, 2026
@elasticmachine
Copy link
Copy Markdown
Contributor

Pinging @elastic/elastic-agent-data-plane (Team:Elastic-Agent-Data-Plane)

@botelastic botelastic bot removed the needs_team Indicates that the issue/PR needs a Team:* label label Mar 30, 2026
@github-actions

This comment has been minimized.

@github-actions
Copy link
Copy Markdown
Contributor

TL;DR

The Buildkite failures are deterministic test regressions in libbeat/processors/actions caused by error-message casing drift ("Failed ..." expected vs "failed ..." actual). Aligning processor error strings with existing test expectations should unblock all 4 failed jobs.

Remediation

  • In libbeat/processors/actions/decompress_gzip_field.go and libbeat/processors/actions/rename.go, restore the error prefixes to sentence case ("Failed ...") for the error.message values produced in Run(...).
  • Re-run libbeat suites that failed in CI (mage build unitTest, mage goUnitTest, mage goFIPSOnlyUnitTest, mage goIntegTest) to confirm all assertions pass.
Investigation details

Root Cause

All failed jobs point to assertion mismatches on exact error.message string values in tests that compare full event maps.

  • libbeat/processors/actions/decompress_gzip_field_test.go:188
  • libbeat/processors/actions/rename_test.go:253

The failures show the same pattern: expected message starts with "Failed ...", actual starts with "failed ...".

Evidence

  • Build: https://buildkite.com/elastic/beats/builds/43261
  • Failed jobs:
    • Libbeat: Ubuntu x86_64 Unit Tests
    • Libbeat: Ubuntu x86_64 Go Unit Tests with fips provider and requirefips build tag
    • Libbeat: Ubuntu x86_64 fips140=only Unit Tests
    • Libbeat: Go Integration Tests
  • Key log excerpts:
expected: {"error":{"message":"Failed to decompress field in decompress_gzip_field processor: ..."}}
actual  : {"error":{"message":"failed to decompress field in decompress_gzip_field processor: ..."}}
rename_test.go:253: Error: Should be true

TestRenameRun compares full maps (reflect.DeepEqual), so message-casing changes in error.message fail the test.

Verification

  • Tests not re-run locally in this workflow (analysis based on Buildkite logs and repository source inspection).

Follow-up

If lowercase wording is intentional, update test fixtures in:

  • libbeat/processors/actions/decompress_gzip_field_test.go
  • libbeat/processors/actions/rename_test.go

Otherwise, keep backward-compatible error.message text ("Failed ...") in processors.

Note

🔒 Integrity filtering filtered 2 items

Integrity filtering activated and filtered the following items during workflow execution.
This happens when a tool call accesses a resource that does not meet the required integrity or secrecy level of the workflow.


What is this? | From workflow: PR Buildkite Detective

Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.

@strawgate strawgate force-pushed the perf/skip-unnecessary-clone branch from 8b061ee to 5f260f5 Compare March 31, 2026 15:38
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@libbeat/processors/dissect/processor.go`:
- Around line 138-140: The loop over m currently swallows errors from
event.PutValue (for k, v := range m { _, _ = event.PutValue(prefix+k, v) }),
producing possible silent partial writes; change it to capture the returned
error from event.PutValue and propagate the first non-nil error (e.g., if err :=
event.PutValue(prefix+k, v); err != nil { return err }) so the caller of this
dissect processor sees failures consistent with other processors in this PR.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c339d836-d4da-4cd0-864c-7e7593c78690

📥 Commits

Reviewing files that changed from the base of the PR and between 8b061ee and 5f260f5.

📒 Files selected for processing (13)
  • changelog/fragments/1774840000-skip-unnecessary-event-clone.yaml
  • libbeat/processors/actions/append.go
  • libbeat/processors/actions/clone_skip_test.go
  • libbeat/processors/actions/copy_fields.go
  • libbeat/processors/actions/decode_base64_field.go
  • libbeat/processors/actions/decompress_gzip_field.go
  • libbeat/processors/actions/decompress_gzip_field_test.go
  • libbeat/processors/actions/replace.go
  • libbeat/processors/actions/truncate_fields.go
  • libbeat/processors/decode_csv_fields/decode_csv_fields.go
  • libbeat/processors/dissect/processor.go
  • libbeat/processors/extract_array/extract_array.go
  • libbeat/processors/urldecode/urldecode.go
💤 Files with no reviewable changes (1)
  • libbeat/processors/actions/decode_base64_field.go
✅ Files skipped from review due to trivial changes (2)
  • changelog/fragments/1774840000-skip-unnecessary-event-clone.yaml
  • libbeat/processors/actions/decompress_gzip_field_test.go
🚧 Files skipped from review as they are similar to previous changes (6)
  • libbeat/processors/actions/decompress_gzip_field.go
  • libbeat/processors/extract_array/extract_array.go
  • libbeat/processors/decode_csv_fields/decode_csv_fields.go
  • libbeat/processors/actions/replace.go
  • libbeat/processors/actions/clone_skip_test.go
  • libbeat/processors/actions/append.go

Eliminate event.Clone() in dissect (check-then-write), rename, copy,
replace, truncate, urldecode, extract_array, decode_csv, decode_base64,
decompress_gzip, and append processors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@strawgate strawgate force-pushed the perf/skip-unnecessary-clone branch from 5f260f5 to 5c0e967 Compare March 31, 2026 15:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport-skip Skip notification from the automated backport with mergify skip-changelog Team:Elastic-Agent-Data-Plane Label for the Agent Data Plane team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants