diff --git a/libbeat/otelbeat/otelmap/otelmap.go b/libbeat/otelbeat/otelmap/otelmap.go index 50487d66a56c..45e2e913f4e5 100644 --- a/libbeat/otelbeat/otelmap/otelmap.go +++ b/libbeat/otelbeat/otelmap/otelmap.go @@ -19,6 +19,8 @@ package otelmap import ( + "encoding" + "encoding/json" "fmt" "reflect" "time" @@ -86,6 +88,13 @@ func ConvertNonPrimitive[T mapstrOrMap](m T) { s = append(s, time.Time(i).UTC().Format("2006-01-02T15:04:05.000Z")) } m[key] = s + case encoding.TextMarshaler: + text, err := x.MarshalText() + if err != nil { + m[key] = fmt.Sprintf("error converting %T to string: %s", x, err) + continue + } + m[key] = string(text) case []bool, []string, []float32, []float64, []int, []int8, []int16, []int32, []int64, []uint, []uint8, []uint16, []uint32, []uint64: ref := reflect.ValueOf(x) @@ -97,6 +106,17 @@ func ConvertNonPrimitive[T mapstrOrMap](m T) { case nil, string, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, bool: default: ref := reflect.ValueOf(x) + if ref.Kind() == reflect.Struct { + var im map[string]any + err := marshalUnmarshal(x, &im) + if err != nil { + m[key] = fmt.Sprintf("error encoding struct to map: %s", err) + continue + } + ConvertNonPrimitive(im) + m[key] = im + break + } if ref.Kind() == reflect.Slice || ref.Kind() == reflect.Array { s := make([]any, ref.Len()) for i := 0; i < ref.Len(); i++ { @@ -114,8 +134,24 @@ func ConvertNonPrimitive[T mapstrOrMap](m T) { m[key] = s break // we figured out the type, so we don't need the unknown type case } - m[key] = fmt.Sprintf("unknown type: %T", x) } } } + +// marshalUnmarshal converts an interface to a mapstr.M by marshalling to JSON +// then unmarshalling the JSON object into a mapstr.M. +// Copied from libbeat/common/event.go +func marshalUnmarshal(in interface{}, out interface{}) error { + // Decode and encode as JSON to normalize the types. + marshaled, err := json.Marshal(in) + if err != nil { + return fmt.Errorf("error marshalling to JSON: %w", err) + } + err = json.Unmarshal(marshaled, out) + if err != nil { + return fmt.Errorf("error unmarshalling from JSON: %w", err) + } + + return nil +} diff --git a/libbeat/otelbeat/otelmap/otelmap_test.go b/libbeat/otelbeat/otelmap/otelmap_test.go index 5d2b19bd35fa..b7cd618edfa3 100644 --- a/libbeat/otelbeat/otelmap/otelmap_test.go +++ b/libbeat/otelbeat/otelmap/otelmap_test.go @@ -273,12 +273,28 @@ func TestFromMapstrSliceCommonTime(t *testing.T) { assert.Equal(t, want, inputMap) } +type structWithTextMarshaler struct { + Value string `json:"value"` +} + +func (s *structWithTextMarshaler) MarshalText() ([]byte, error) { + return []byte("marshalled:" + s.Value), nil +} + func TestFromMapstrWithNestedData(t *testing.T) { input := mapstr.M{ "any_array": [3]any{1, "string", 3}, "any_slice": []any{5.1, 6.2}, "bool_array": [2]bool{true, false}, "bool_slice": []bool{false, true}, + "struct": struct { + Value string `json:"value"` + }{ + Value: "string", + }, + "struct_with_text_marshaler": &structWithTextMarshaler{ + Value: "string", + }, "inner": []mapstr.M{ { "inner_int": 42, @@ -287,6 +303,14 @@ func TestFromMapstrWithNestedData(t *testing.T) { {"string": "string"}, {"number": 12.3}, }, + "inner_struct": struct { + Value string `json:"value"` + }{ + Value: "string", + }, + "inner_struct_with_text_marshaler": &structWithTextMarshaler{ + Value: "string", + }, }, { "inner_int": 43, @@ -306,6 +330,10 @@ func TestFromMapstrWithNestedData(t *testing.T) { "any_slice": []any{5.1, 6.2}, "bool_array": []any{true, false}, "bool_slice": []any{false, true}, + "struct": map[string]any{ + "value": "string", + }, + "struct_with_text_marshaler": "marshalled:string", "inner": []any{ map[string]any{ "inner_int": 42, @@ -314,6 +342,10 @@ func TestFromMapstrWithNestedData(t *testing.T) { map[string]any{"string": "string"}, map[string]any{"number": 12.3}, }, + "inner_struct": map[string]any{ + "value": "string", + }, + "inner_struct_with_text_marshaler": "marshalled:string", }, map[string]any{ "inner_int": 43, @@ -363,24 +395,12 @@ func TestToMapstr(t *testing.T) { assert.Equal(t, want, got) } -type unknown struct { - Value int `json:"value"` -} - func TestUnknownType(t *testing.T) { inputMap := mapstr.M{ - "unknown": unknown{42}, - "nested": mapstr.M{ - "unknown": unknown{43}, - }, "unknown_map": map[string]int{"key": 42}, } expected := mapstr.M{ - "unknown": "unknown type: otelmap.unknown", - "nested": map[string]any{ - "unknown": "unknown type: otelmap.unknown", - }, "unknown_map": "unknown type: map[string]int", }