Skip to content

Commit

Permalink
fix more stdlib functions working with targets (#2917)
Browse files Browse the repository at this point in the history
* fix more stdlib functions working with targets

* fix more stdlib functions working with targets

* fix more stdlib functions working with targets
  • Loading branch information
thampiotr authored Mar 6, 2025
1 parent 6810350 commit e7c7a88
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 101 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ Main (unreleased)

- Fixed an issue where passing targets from some standard library functions was failing with `target::ConvertFrom` error. (@thampiotr)

- Fixed `expected object or array, got capsule` errors that could be encountered when using targets with `coalesce` and
`array.combine_maps` functions. (@thampiotr)

### Other changes

- Upgrading to Prometheus v2.55.1. (@ptodev)
Expand Down
102 changes: 65 additions & 37 deletions internal/component/discovery/target_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,56 +23,56 @@ import (

func TestUsingTargetCapsule(t *testing.T) {
type testCase struct {
name string
inputTarget map[string]string
expression string
decodeInto interface{}
decodedString string
expectedEvalError string
name string
inputTarget map[string]string
expression string
decodeInto interface{}
expectedDecodedString string
expectedEvalError string
}

testCases := []testCase{
{
name: "target to map of string -> string",
inputTarget: map[string]string{"a1a": "beachfront avenue", "ice": "ice"},
expression: "t",
decodeInto: map[string]string{},
decodedString: `{"a1a"="beachfront avenue", "ice"="ice"}`,
name: "target to map of string -> string",
inputTarget: map[string]string{"a1a": "beachfront avenue", "ice": "ice"},
expression: "t",
decodeInto: map[string]string{},
expectedDecodedString: `{"a1a"="beachfront avenue", "ice"="ice"}`,
},
{
name: "target to map of string -> any",
inputTarget: map[string]string{"a1a": "beachfront avenue", "ice": "ice"},
expression: "t",
decodeInto: map[string]any{},
decodedString: `{"a1a"="beachfront avenue", "ice"="ice"}`,
name: "target to map of string -> any",
inputTarget: map[string]string{"a1a": "beachfront avenue", "ice": "ice"},
expression: "t",
decodeInto: map[string]any{},
expectedDecodedString: `{"a1a"="beachfront avenue", "ice"="ice"}`,
},
{
name: "target to map of any -> any",
inputTarget: map[string]string{"a1a": "beachfront avenue", "ice": "ice"},
expression: "t",
decodeInto: map[any]any{},
decodedString: `{"a1a"="beachfront avenue", "ice"="ice"}`,
name: "target to map of any -> any",
inputTarget: map[string]string{"a1a": "beachfront avenue", "ice": "ice"},
expression: "t",
decodeInto: map[any]any{},
expectedDecodedString: `{"a1a"="beachfront avenue", "ice"="ice"}`,
},
{
name: "target to map of string -> syntax.Value",
inputTarget: map[string]string{"a1a": "beachfront avenue"},
expression: "t",
decodeInto: map[string]syntax.Value{},
decodedString: `{"a1a"="beachfront avenue"}`,
name: "target to map of string -> syntax.Value",
inputTarget: map[string]string{"a1a": "beachfront avenue"},
expression: "t",
decodeInto: map[string]syntax.Value{},
expectedDecodedString: `{"a1a"="beachfront avenue"}`,
},
{
name: "target indexing a string value",
inputTarget: map[string]string{"a1a": "beachfront avenue", "hip": "hop"},
expression: `t["hip"]`,
decodeInto: "",
decodedString: `hop`,
name: "target indexing a string value",
inputTarget: map[string]string{"a1a": "beachfront avenue", "hip": "hop"},
expression: `t["hip"]`,
decodeInto: "",
expectedDecodedString: `hop`,
},
{
name: "target indexing a non-existing string value",
inputTarget: map[string]string{"a1a": "beachfront avenue", "hip": "hop"},
expression: `t["boom"]`,
decodeInto: "",
decodedString: "<nil>",
name: "target indexing a non-existing string value",
inputTarget: map[string]string{"a1a": "beachfront avenue", "hip": "hop"},
expression: `t["boom"]`,
decodeInto: "",
expectedDecodedString: "<nil>",
},
{
name: "target indexing a value like an object field",
Expand All @@ -81,6 +81,34 @@ func TestUsingTargetCapsule(t *testing.T) {
decodeInto: "",
expectedEvalError: `field "boom" does not exist`,
},
{
name: "targets passed to concat",
inputTarget: map[string]string{"boom": "bap", "hip": "hop"},
expression: `array.concat([t], [t])`,
decodeInto: []Target{},
expectedDecodedString: `[{"boom"="bap", "hip"="hop"} {"boom"="bap", "hip"="hop"}]`,
},
{
name: "coalesce an empty target",
inputTarget: map[string]string{},
expression: `coalesce(t, [], t, {}, t, 123, t)`,
decodeInto: []Target{},
expectedDecodedString: `123`,
},
{
name: "coalesce a non-empty target",
inputTarget: map[string]string{"big": "bang"},
expression: `coalesce([], {}, "", t, 321, [])`,
decodeInto: []Target{},
expectedDecodedString: `{"big"="bang"}`,
},
{
name: "array.combine_maps with targets",
inputTarget: map[string]string{"a": "a1", "b": "b1"},
expression: `array.combine_maps([t, t], [{"a" = "a1", "c" = "c1"}, {"a" = "a2", "c" = "c2"}], ["a"])`,
decodeInto: []Target{},
expectedDecodedString: `[map[a:a1 b:b1 c:c1] map[a:a1 b:b1 c:c1]]`,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
Expand All @@ -95,7 +123,7 @@ func TestUsingTargetCapsule(t *testing.T) {
} else {
require.NoError(t, evalError)
}
require.Equal(t, tc.decodedString, fmt.Sprintf("%v", tc.decodeInto))
require.Equal(t, tc.expectedDecodedString, fmt.Sprintf("%v", tc.decodeInto))
})
}
}
Expand Down
9 changes: 3 additions & 6 deletions syntax/encoding/alloyjson/alloyjson.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,12 +287,9 @@ func buildJSONValue(v value.Value) jsonValue {
return jsonValue{Type: "function", Value: v.Describe()}

case value.TypeCapsule:
if v.Implements(reflect.TypeFor[value.ConvertibleIntoCapsule]()) {
// Check if this capsule can be converted into Alloy object for more detailed description:
newVal := make(map[string]value.Value)
if err := v.ReflectAddr().Interface().(value.ConvertibleIntoCapsule).ConvertInto(&newVal); err == nil {
return tokenizeObject(value.Encode(newVal))
}
// Check if this capsule can be converted into Alloy object for more detailed description:
if newVal, ok := v.TryConvertToObject(); ok {
return tokenizeObject(value.Encode(newVal))
}
// Otherwise, describe the value
return jsonValue{Type: "capsule", Value: v.Describe()}
Expand Down
58 changes: 38 additions & 20 deletions syntax/internal/stdlib/stdlib.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,13 @@ import (
"github.com/grafana/alloy/syntax/internal/value"
)

// TODO: refactor the stdlib to have consistent naming between namespaces and identifiers.

// ExperimentalIdentifiers contains the full name (namespace + identifier's name) of stdlib
// identifiers that are considered "experimental".
var ExperimentalIdentifiers = map[string]bool{
"array.combine_maps": true,
}

// These identifiers are deprecated in favour of the namespaced ones.
// DeprecatedIdentifiers are deprecated in favour of the namespaced ones.
var DeprecatedIdentifiers = map[string]interface{}{
"env": os.Getenv,
"nonsensitive": nonSensitive,
Expand Down Expand Up @@ -228,15 +226,19 @@ var combineMaps = value.RawFunction(func(funcValue value.Value, args ...value.Va
}
}
for j := 0; j < args[i].Len(); j++ {
if args[i].Index(j).Type() != value.TypeObject {
return value.Null, value.ArgError{
Function: funcValue,
Argument: args[i].Index(j),
Index: j,
Inner: value.TypeError{
Value: args[i].Index(j),
Expected: value.TypeObject,
},
elem := args[i].Index(j)
// Check if elements are objects or are convertible to objects.
if elem.Type() != value.TypeObject {
if _, ok := elem.TryConvertToObject(); !ok {
return value.Null, value.ArgError{
Function: funcValue,
Argument: elem,
Index: j,
Inner: value.TypeError{
Value: elem,
Expected: value.TypeObject,
},
}
}
}
}
Expand All @@ -263,15 +265,23 @@ var combineMaps = value.RawFunction(func(funcValue value.Value, args ...value.Va
}
}

convertIfNeeded := func(v value.Value) value.Value {
if v.Type() != value.TypeObject {
obj, _ := v.TryConvertToObject() // no need to check result as arguments were validated earlier.
return value.Object(obj)
}
return v
}

// We cannot preallocate the size of the result array, because we don't know
// how well the merge is going to go. If none of the merge conditions are met,
// the result array will be empty.
res := []value.Value{}

for i := 0; i < args[0].Len(); i++ {
for j := 0; j < args[1].Len(); j++ {
left := args[0].Index(i)
right := args[1].Index(j)
left := convertIfNeeded(args[0].Index(i))
right := convertIfNeeded(args[1].Index(j))

join, err := shouldJoin(left, right, args[2])
if err != nil {
Expand Down Expand Up @@ -309,33 +319,33 @@ func yamlDecode(in string) (interface{}, error) {
return res, nil
}

func base64Decode(in string) (interface{}, error) {
func base64Decode(in string) ([]byte, error) {
decoded, err := base64.StdEncoding.DecodeString(in)
if err != nil {
return nil, err
}
return decoded, nil
}

func base64URLDecode(in string) (interface{}, error) {
func base64URLDecode(in string) ([]byte, error) {
decoded, err := base64.URLEncoding.DecodeString(in)
if err != nil {
return nil, err
}
return decoded, nil
}

func base64URLEncode(in string) (interface{}, error) {
func base64URLEncode(in string) (string, error) {
encoded := base64.URLEncoding.EncodeToString([]byte(in))
return encoded, nil
}

func base64Encode(in string) (interface{}, error) {
func base64Encode(in string) (string, error) {
encoded := base64.StdEncoding.EncodeToString([]byte(in))
return encoded, nil
}

func jsonPath(jsonString string, path string) (interface{}, error) {
func jsonPath(jsonString string, path string) ([]interface{}, error) {
jsonPathExpr, err := jp.ParseString(path)
if err != nil {
return nil, err
Expand All @@ -360,13 +370,21 @@ var coalesce = value.RawFunction(func(funcValue value.Value, args ...value.Value
}

if !arg.Reflect().IsZero() {
if argType := value.AlloyType(arg.Reflect().Type()); (argType == value.TypeArray || argType == value.TypeObject) && arg.Len() == 0 {
argType := arg.Type()
// Check if it's a capsule that can be converted into an object and the object is empty.
if obj, ok := arg.TryConvertToObject(); ok && len(obj) == 0 {
continue
}
// Check if it's an array or an object that's empty.
if (argType == value.TypeArray || argType == value.TypeObject) && arg.Len() == 0 {
continue
}

// Else we found a non-empty argument.
return arg, nil
}
}

// Return the last arg if all are empty.
return args[len(args)-1], nil
})
12 changes: 12 additions & 0 deletions syntax/internal/value/value.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,18 @@ func (v Value) ReflectAddr() reflect.Value {
return addrRV
}

// TryConvertToObject will try to convert v into an Alloy object in a map[string]Value form. Returns (object, true) if
// successful or (nil, false) if conversion was not possible.
func (v Value) TryConvertToObject() (map[string]Value, bool) {
if v.Type() == TypeCapsule && v.Implements(reflect.TypeFor[ConvertibleIntoCapsule]()) {
objVal := make(map[string]Value)
if err := v.ReflectAddr().Interface().(ConvertibleIntoCapsule).ConvertInto(&objVal); err == nil {
return objVal, true
}
}
return nil, false
}

// makeValue converts a reflect value into a Value, dereferencing any pointers or
// interface{} values.
func makeValue(v reflect.Value) Value {
Expand Down
15 changes: 4 additions & 11 deletions syntax/token/builder/value_tokens.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package builder

import (
"fmt"
"reflect"
"sort"

"github.com/grafana/alloy/syntax/internal/value"
Expand Down Expand Up @@ -64,16 +63,10 @@ func valueTokens(v value.Value) []Token {
toks = append(toks, Token{token.LITERAL, v.Describe()})

case value.TypeCapsule:
done := false
if v.Implements(reflect.TypeFor[value.ConvertibleIntoCapsule]()) {
// Check if this capsule can be converted into Alloy object for more detailed description:
newVal := make(map[string]value.Value)
if err := v.ReflectAddr().Interface().(value.ConvertibleIntoCapsule).ConvertInto(&newVal); err == nil {
toks = tokenEncode(newVal)
done = true
}
}
if !done {
// Check if this capsule can be converted into Alloy object for more detailed description:
if newVal, ok := v.TryConvertToObject(); ok {
toks = tokenEncode(newVal)
} else {
// Default to Describe() for capsules that don't support other representation.
toks = append(toks, Token{token.LITERAL, v.Describe()})
}
Expand Down
Loading

0 comments on commit e7c7a88

Please sign in to comment.