Skip to content

Conversation

@trevorwhitney
Copy link
Collaborator

What this PR does / why we need it:

This PR continues our efforts towards LogQL backwards compatibility, with a focus on queries needed for Drilldown, by implementing support for unwrap. Unwrap was implemented as a projection.

Special notes for your reviewer:

Checklist

  • Reviewed the CONTRIBUTING.md guide (required)
  • Documentation added
  • Tests updated
  • Title matches the required conventional commits format, see here
    • Note that Promtail is considered to be feature complete, and future development for logs collection will be in Grafana Alloy. As such, feat PRs are unlikely to be accepted unless a case can be made for the feature actually being a bug fix to existing behavior.
  • Changes that require user attention or interaction to upgrade are documented in docs/sources/setup/upgrade/_index.md
  • If the change is deprecating or removing a configuration option, update the deprecated-config.yaml and deleted-config.yaml files respectively in the tools/deprecated-config-checker directory. Example PR

@trevorwhitney trevorwhitney requested a review from a team as a code owner October 7, 2025 20:32
@trevorwhitney trevorwhitney force-pushed the twhitney/thor-unwrap branch 2 times, most recently from ba88390 to 0f7c19b Compare October 8, 2025 22:05
Comment on lines 18 to 19
UnaryOpUnwrapBytes // Unwrap string bytes to float value operation (unwrap).
UnaryOpUnwrapDuration // Unwrap string duration to float value operation (unwrap).
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we should have individual operations for unwrap bytes and unwrap duration.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

without the different types, how do we determine the correct conversion function? would you parse that during logical planning and pass that information through in a way other than the operation? the pattern for registering functions leaned heavily on operation types, so it seemed like a good place to make the differentiation to me.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@chaudum and I agreed offline we'll need different operations, but they should be renamed to cast operations as that's what these actually represent, ie. how to do the cast.

const (
// UnwrapOpInvalid indicates an invalid unwrap operation.
UnwrapOpInvalid UnwrapOp = iota
Unwrap
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this considered an unwrap INT or unwrap FLOAT, or generic unwrap NUMBER?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Unwrap would be a unwrap FLOAT -> strconv.ParseFloat(v, 64)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

renamed to cast and got made the base case more explicit by naming it CastFloat

Copy link
Member

@rfratto rfratto left a comment

Choose a reason for hiding this comment

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

Nice work! I really like how this flows as a projection and unary expression.

Comment on lines +17 to +19
UnaryOpCastFloat // Cast string to float value operation (unwrap).
UnaryOpCastBytes // Cast string bytes to float value operation (unwrap).
UnaryOpCastDuration // Cast string duration to float value operation (unwrap).
Copy link
Member

Choose a reason for hiding this comment

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

all of these say "to float value", but is that correct?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think so, because the type of the value column the casted result is put in will always be a float64, and the conversion functions all return a float64. The difference is in how they get that value (ie. strconv, time.Parse, or humanize)

Comment on lines 52 to 53
// Unwrap applies an [Projection] operation, with an [UnaryOp] unwrap operation, to the Builder.
func (b *Builder) Unwrap(identifier string, operation string) *Builder {
Copy link
Member

Choose a reason for hiding this comment

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

Hm, I intended Builder to have an API which more closely reflects the logical plan rather than LogQL constructs.

Can we update this to Builder.Cast maybe?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

oh, interesting, I though of builder as the bridge between logql and the logical plan, so the parameters of this function match closely to what we get out of logql for the unwrap operation. I'm happy to go with Cast though, as I think the arguments are still consistent.

Comment on lines 260 to 264
// Check for unwrap in the LogRangeExpr
if e.Left.Unwrap != nil {
unwrapIdentifier = e.Left.Unwrap.Identifier
unwrapOperation = e.Left.Unwrap.Operation
}
Copy link
Member

Choose a reason for hiding this comment

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

Is it valid for a LogQL query to have more than one unwrap? If so, this breaks.

Maybe in the meantime, we can return errUnimplemented if we see an unwrap but unwrapIdentifier is already set?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I can return errUnimplemented 👍 . The only way I can think of having multiple unwrap statements is on opposite sides of a binary operation, so in two separate range expressions. I haven't thought about how that would be handled in this PR.

// Check for unwrap in the LogRangeExpr
if e.Left.Unwrap != nil {
unwrapIdentifier = e.Left.Unwrap.Identifier
unwrapOperation = e.Left.Unwrap.Operation
Copy link
Member

Choose a reason for hiding this comment

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

IMO we should be validating that we recognize and support whatever e.Left.Unwrap.Operation is and return errUnimplmeneted otherwise.

}
}

schema := arrow.NewSchema(fields, nil)
Copy link
Member

Choose a reason for hiding this comment

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

Should this propagate the metadata from the input? Are we using metadata anywhere anymore? (Ditto with newKeepPipeline above)

}

func newExpandPipeline(expressions []physical.UnaryExpression, evaluator *expressionEvaluator, input Pipeline) (*GenericPipeline, error) {
return newGenericPipeline(func(ctx context.Context, inputs []Pipeline) (arrow.Record, error) {
Copy link
Member

Choose a reason for hiding this comment

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

I think ideally we validate that len(inputs) == 1 here so things don't silently break. Ditto with newKeepPipeline

pipeline := NewFilterPipeline(filter, input, expressionEvaluator{}, alloc)
defer pipeline.Close()
e := newExpressionEvaluator(alloc)
pipeline := NewFilterPipeline(filter, input, e, alloc)
Copy link
Member

Choose a reason for hiding this comment

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

I think we're missing a defer pipeline.Close() here, it was there but got removed in the diff

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ahh, good catch. many rebases

Comment on lines +35 to +38
if errTracker.hasErrors {
defer errorCol.Release()
defer errorDetailsCol.Release()
}
Copy link
Member

Choose a reason for hiding this comment

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

You can remove this since that's already handled by the defer to releaseBuilders on the line above

Comment on lines 97 to 117
func buildOutputFields(
hasErrors bool,
) []arrow.Field {
fields := make([]arrow.Field, 0, 3)

// Add value field. Not nullable in practice since we use 0.0 when conversion fails, but as of
// writing all coumns are marked as nullable, even Timestamp and Message, so staying consistent
fields = append(fields, semconv.FieldFromIdent(semconv.ColumnIdentValue, true))

// Add error fields if needed
if hasErrors {
fields = append(fields,
semconv.FieldFromIdent(semconv.ColumnIdentError, true),
semconv.FieldFromIdent(semconv.ColumnIdentErrorDetails, true),
)
}

return fields
}

func buildResult(
Copy link
Member

Choose a reason for hiding this comment

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

buildOutputFields and buildResult might be a little too generically named for being in the executorPackage, can we give them more precise names?

Copy link
Contributor

@chaudum chaudum left a comment

Choose a reason for hiding this comment

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

Nothing more to add than what has already been said.

@trevorwhitney trevorwhitney merged commit 3ce6fa2 into main Oct 17, 2025
63 checks passed
@trevorwhitney trevorwhitney deleted the twhitney/thor-unwrap branch October 17, 2025 20:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants