Skip to content

app/vlagent/remotewrite: introduce VictoriaLogs transformations language#1508

Open
vadimalekseev wants to merge 1 commit into
masterfrom
vlt
Open

app/vlagent/remotewrite: introduce VictoriaLogs transformations language#1508
vadimalekseev wants to merge 1 commit into
masterfrom
vlt

Conversation

@vadimalekseev

@vadimalekseev vadimalekseev commented Jun 15, 2026

Copy link
Copy Markdown
Member

@vadimalekseev
vadimalekseev marked this pull request as draft June 15, 2026 14:45

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

3 issues found across 33 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread lib/logstorage/log_rows.go
Comment thread lib/logstorage/transforms.go Outdated
Comment thread lib/logstorage/block_result.go
@vadimalekseev
vadimalekseev force-pushed the vlt branch 5 times, most recently from 1e493dd to e300516 Compare June 22, 2026 10:56
@vadimalekseev
vadimalekseev force-pushed the vlt branch 5 times, most recently from 39f6deb to c3556c4 Compare June 25, 2026 06:58
@vadimalekseev
vadimalekseev marked this pull request as ready for review June 25, 2026 06:58

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

4 issues found and verified against the latest diff

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread lib/logstorage/log_rows.go Outdated
Comment thread lib/logstorage/log_rows.go Outdated
Comment thread lib/logstorage/block_result.go
Comment thread lib/logstorage/transforms.go Outdated
fields := rows[0]
for i := range fields {
name := br.addValue(fields[i].Name)
name := getCanonicalColumnName(fields[i].Name)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

These changes are not related to log transformations, but without them, my tests fail.

@vadimalekseev
vadimalekseev force-pushed the vlt branch 3 times, most recently from 8ad3249 to e286231 Compare June 25, 2026 08:56
Comment thread lib/logstorage/log_rows.go Outdated
//
// It stops calling visitFunc on the remaining filters as soon as visitFunc returns true.
// It returns the result of the last visitFunc call.
func visitFilterRecursiveForPipe(p pipe, visitFunc func(f filter) bool) bool {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This code was written in the hope that it would be replaced with supporting relative time filters in VictoriaLogs transformations.

@vadimalekseev
vadimalekseev force-pushed the vlt branch 2 times, most recently from fa0bd2b to 2e51424 Compare June 26, 2026 12:16
Comment thread lib/logstorage/block_result.go Outdated
Comment thread lib/logstorage/log_rows.go Outdated
timestamps = br.getTimestamps()
} else {
// Slow path: the _time column was overwritten or removed, initialize it again.
br.initTimestamps()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Inside initTimestamps, if brSrc is not nil, it takes the timestamps from brSrc. So if our blockResult has an overwritten _time column, that column is ignored:

func (br *blockResult) initTimestamps() {
if br.brSrc != nil {
srcTimestamps := br.brSrc.getTimestamps()
br.initTimestampsInternal(srcTimestamps)
return

In an if branch, when the filter matches only part of the block, a new blockResult is created with the brSrc set:

brMatched.initFromFilterAllColumns(br, bmMatched)
brUnmatched.initFromFilterAllColumns(br, bmUnmatched)

func (br *blockResult) initFromFilterAllColumns(brSrc *blockResult, bm *bitmap) {
br.reset()
br.rowsLen = bm.onesCount()
if br.rowsLen == 0 {
return
}
br.brSrc = brSrc

Combining both, with a filter like:

if (service:=payment) {
  time_add 1h;
}

on this data:

{"_time":"2026-07-01T10:00:00Z","service":"products","_msg":"a"}
{"_time":"2026-07-01T10:05:00Z","service":"products","_msg":"b"}
{"_time":"2026-07-01T10:00:00Z","service":"payment","_msg":"c"}
{"_time":"2026-07-01T10:05:00Z","service":"payment","_msg":"d"}

The payment rows keep their original _time instead of the one time_add produced

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thank you! Fixed.
br.getTimestamps now initializes timestamps if timestampBuf is empty (original behavior). Additionally, it parses the _time column if timestampBuf is not empty (initialized) but contains outdated values (isTime == false).

It looks like either I am misusing getTimestamps(), or the issue is also present in the original code. Could you please validate the new behavior?

@cuongleqq cuongleqq Jul 8, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I checked the other callers of getTimestamps() - they all check c.isTime first before calling it. So getTimestamps() is only called when _time still holds the real timestamps. That is why this problem does not happen elsewhere.

About the new fix: time_add works now, but format / copy / rename ... as _time still do not. time_add fills timestampsBuf first to read time, so it's not empty. But other pipes only create new _time columns without reading _time first, so timestampsBuf is empty in these cases.

One solution to fix this could be to change initFromBlockResult. Maybe we only call br.getTimestamps() if the column _time is still isTime, or else we parse it? I am not very sure this is best in term of performance, but it will be correct since it just copies from how other pipes do it before calling getTimestamps().

@vadimalekseev
vadimalekseev force-pushed the vlt branch 3 times, most recently from 2f55dd2 to 4bba6e3 Compare July 6, 2026 15:14
@vadimalekseev
vadimalekseev force-pushed the vlt branch 2 times, most recently from 366ec73 to b3ffaeb Compare July 6, 2026 15:26
Comment thread lib/logstorage/transforms.go Outdated
@vadimalekseev
vadimalekseev force-pushed the vlt branch 2 times, most recently from f05c913 to d940917 Compare July 8, 2026 09:54
putColumnIdxs(columnIdxs)
}

func (br *blockResult) mustInitFromLogRows(lr *LogRows) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Minor, not a change request: this name reads like a generic LogRows→blockResult converter, but it actually contains transform-specific logic (vl_account_id / vl_project_id, and it's only called from the transformer). This might cause a little confusion or misuse later. Same for initFromBlockResult. Feel free to resolve, just flagging it.

so the target tenant is controlled through Kubernetes annotations.

If `vl_account_id` and `vl_project_id` fields arrive in the incoming log,
`vlagent` treats them as ordinary fields and does not change the tenant.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

treats them as ordinary fields

The docs might be wrong here. From the code in initFromBlockResult, these fields always stripped from the result.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The documentation is correct, as mustInitFromLogRows overrides user-defined vl_account_id and vl_project_id fields:

  1. We call addResultColumn here:
    br.addResultColumn(resultColumn{name: "vl_project_id", values: projectIDs})
  2. addResultColumn replaces existing column:
    br.csAddOrReplace(&csBuf[len(csBuf)-1])

This behavior is intentional: if a user enabled log transformations and passed vl_account_id in the request, but later disabled transformations, the logs would be written to the default tenant (because we won't call the initFromBlockResult function). The current approach is more explicit: if you want to set the tenant dynamically, you must provide it and set up an explicit transformation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support for Log Transformations in vlagent

2 participants