Skip to content

Commit 2685f9c

Browse files
authored
otelmap: handle conversion of struct types (#45234)
The otelmap package does not handle struct types properly, the convertion of a struct results in a string error. To follow the libbeat behavior, handle struct types the same way, by serializing to json and back, resulting in a map of keys and values.
1 parent b2b2aa8 commit 2685f9c

File tree

2 files changed

+69
-13
lines changed

2 files changed

+69
-13
lines changed

libbeat/otelbeat/otelmap/otelmap.go

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
package otelmap
2020

2121
import (
22+
"encoding"
23+
"encoding/json"
2224
"fmt"
2325
"reflect"
2426
"time"
@@ -86,6 +88,13 @@ func ConvertNonPrimitive[T mapstrOrMap](m T) {
8688
s = append(s, time.Time(i).UTC().Format("2006-01-02T15:04:05.000Z"))
8789
}
8890
m[key] = s
91+
case encoding.TextMarshaler:
92+
text, err := x.MarshalText()
93+
if err != nil {
94+
m[key] = fmt.Sprintf("error converting %T to string: %s", x, err)
95+
continue
96+
}
97+
m[key] = string(text)
8998
case []bool, []string, []float32, []float64, []int, []int8, []int16, []int32, []int64,
9099
[]uint, []uint8, []uint16, []uint32, []uint64:
91100
ref := reflect.ValueOf(x)
@@ -97,6 +106,17 @@ func ConvertNonPrimitive[T mapstrOrMap](m T) {
97106
case nil, string, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, bool:
98107
default:
99108
ref := reflect.ValueOf(x)
109+
if ref.Kind() == reflect.Struct {
110+
var im map[string]any
111+
err := marshalUnmarshal(x, &im)
112+
if err != nil {
113+
m[key] = fmt.Sprintf("error encoding struct to map: %s", err)
114+
continue
115+
}
116+
ConvertNonPrimitive(im)
117+
m[key] = im
118+
break
119+
}
100120
if ref.Kind() == reflect.Slice || ref.Kind() == reflect.Array {
101121
s := make([]any, ref.Len())
102122
for i := 0; i < ref.Len(); i++ {
@@ -114,8 +134,24 @@ func ConvertNonPrimitive[T mapstrOrMap](m T) {
114134
m[key] = s
115135
break // we figured out the type, so we don't need the unknown type case
116136
}
117-
118137
m[key] = fmt.Sprintf("unknown type: %T", x)
119138
}
120139
}
121140
}
141+
142+
// marshalUnmarshal converts an interface to a mapstr.M by marshalling to JSON
143+
// then unmarshalling the JSON object into a mapstr.M.
144+
// Copied from libbeat/common/event.go
145+
func marshalUnmarshal(in interface{}, out interface{}) error {
146+
// Decode and encode as JSON to normalize the types.
147+
marshaled, err := json.Marshal(in)
148+
if err != nil {
149+
return fmt.Errorf("error marshalling to JSON: %w", err)
150+
}
151+
err = json.Unmarshal(marshaled, out)
152+
if err != nil {
153+
return fmt.Errorf("error unmarshalling from JSON: %w", err)
154+
}
155+
156+
return nil
157+
}

libbeat/otelbeat/otelmap/otelmap_test.go

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -273,12 +273,28 @@ func TestFromMapstrSliceCommonTime(t *testing.T) {
273273
assert.Equal(t, want, inputMap)
274274
}
275275

276+
type structWithTextMarshaler struct {
277+
Value string `json:"value"`
278+
}
279+
280+
func (s *structWithTextMarshaler) MarshalText() ([]byte, error) {
281+
return []byte("marshalled:" + s.Value), nil
282+
}
283+
276284
func TestFromMapstrWithNestedData(t *testing.T) {
277285
input := mapstr.M{
278286
"any_array": [3]any{1, "string", 3},
279287
"any_slice": []any{5.1, 6.2},
280288
"bool_array": [2]bool{true, false},
281289
"bool_slice": []bool{false, true},
290+
"struct": struct {
291+
Value string `json:"value"`
292+
}{
293+
Value: "string",
294+
},
295+
"struct_with_text_marshaler": &structWithTextMarshaler{
296+
Value: "string",
297+
},
282298
"inner": []mapstr.M{
283299
{
284300
"inner_int": 42,
@@ -287,6 +303,14 @@ func TestFromMapstrWithNestedData(t *testing.T) {
287303
{"string": "string"},
288304
{"number": 12.3},
289305
},
306+
"inner_struct": struct {
307+
Value string `json:"value"`
308+
}{
309+
Value: "string",
310+
},
311+
"inner_struct_with_text_marshaler": &structWithTextMarshaler{
312+
Value: "string",
313+
},
290314
},
291315
{
292316
"inner_int": 43,
@@ -306,6 +330,10 @@ func TestFromMapstrWithNestedData(t *testing.T) {
306330
"any_slice": []any{5.1, 6.2},
307331
"bool_array": []any{true, false},
308332
"bool_slice": []any{false, true},
333+
"struct": map[string]any{
334+
"value": "string",
335+
},
336+
"struct_with_text_marshaler": "marshalled:string",
309337
"inner": []any{
310338
map[string]any{
311339
"inner_int": 42,
@@ -314,6 +342,10 @@ func TestFromMapstrWithNestedData(t *testing.T) {
314342
map[string]any{"string": "string"},
315343
map[string]any{"number": 12.3},
316344
},
345+
"inner_struct": map[string]any{
346+
"value": "string",
347+
},
348+
"inner_struct_with_text_marshaler": "marshalled:string",
317349
},
318350
map[string]any{
319351
"inner_int": 43,
@@ -363,24 +395,12 @@ func TestToMapstr(t *testing.T) {
363395
assert.Equal(t, want, got)
364396
}
365397

366-
type unknown struct {
367-
Value int `json:"value"`
368-
}
369-
370398
func TestUnknownType(t *testing.T) {
371399
inputMap := mapstr.M{
372-
"unknown": unknown{42},
373-
"nested": mapstr.M{
374-
"unknown": unknown{43},
375-
},
376400
"unknown_map": map[string]int{"key": 42},
377401
}
378402

379403
expected := mapstr.M{
380-
"unknown": "unknown type: otelmap.unknown",
381-
"nested": map[string]any{
382-
"unknown": "unknown type: otelmap.unknown",
383-
},
384404
"unknown_map": "unknown type: map[string]int",
385405
}
386406

0 commit comments

Comments
 (0)