diff --git a/pkg/otel/attr.go b/pkg/otel/attr.go index eda0a3e..c542c4d 100644 --- a/pkg/otel/attr.go +++ b/pkg/otel/attr.go @@ -3,6 +3,7 @@ package otel import ( "encoding/json" "fmt" + "reflect" "time" @@ -42,7 +43,7 @@ func marshalMap(rv reflect.Value) ([]attribute.KeyValue, error) { return []attribute.KeyValue{}, nil } if keys[0].Kind() != reflect.String { - return nil, fmt.Errorf("unsupport map key type %s", keys[0].Type()) + return nil, fmt.Errorf("unsupported map key type %s", keys[0].Type()) } attrs := make([]attribute.KeyValue, 0, len(keys)) for index, key := range keys { @@ -145,9 +146,13 @@ func marshalMap(rv reflect.Value) ([]attribute.KeyValue, error) { attrs = append(attrs, attribute.Float64Slice(keyString, value)) case []string: attrs = append(attrs, attribute.StringSlice(keyString, value)) - case time.Time: - attrs = append(attrs, attribute.String(keyString, value.Format(time.RFC3339))) default: + if mv.Type().ConvertibleTo(timeType) { + t := mv.Convert(timeType).Interface().(time.Time) + attrs = append(attrs, attribute.String(keyString, t.Format(time.RFC3339Nano))) + continue + } + kvs, err := marshalField(structFiled{ attributeName: keyString, filedIndex: index, @@ -197,25 +202,20 @@ func marshalField(f structFiled, fv reflect.Value) ([]attribute.KeyValue, error) case reflect.Struct: // convert time.Time to string if fv.Type().ConvertibleTo(timeType) { - return []attribute.KeyValue{ - attribute.String(f.attributeName, fv.Interface().(time.Time).Format(time.RFC3339)), - }, nil + t := fv.Convert(timeType).Interface().(time.Time) + return []attribute.KeyValue{attribute.String(f.attributeName, t.Format(time.RFC3339Nano))}, nil } + return f.marshalNested(fv) - // delve into normal struct types - kvs, err := MarshalOtelAttributes(fv.Interface()) - if err != nil { - return nil, err - } - for i := range kvs { - kvs[i].Key = attribute.Key(f.attributePrefix) + kvs[i].Key - } - return kvs, nil case reflect.Ptr: if fv.IsNil() { return nil, nil } return marshalField(f, fv.Elem()) + + case reflect.Map: + return f.marshalNested(fv) + default: bs, err := json.Marshal(fv.Interface()) if err != nil { @@ -225,6 +225,17 @@ func marshalField(f structFiled, fv reflect.Value) ([]attribute.KeyValue, error) } } +func (s structFiled) marshalNested(fv reflect.Value) ([]attribute.KeyValue, error) { + attrs, err := MarshalOtelAttributes(fv.Interface()) + if err != nil { + return []attribute.KeyValue{}, err + } + for i := range attrs { + attrs[i].Key = attribute.Key(s.attributePrefix) + attrs[i].Key + } + return attrs, nil +} + func marshalSlice(f structFiled, fv reflect.Value) ([]attribute.KeyValue, error) { switch fv.Type().Elem().Kind() { case reflect.Bool: @@ -239,15 +250,14 @@ func marshalSlice(f structFiled, fv reflect.Value) ([]attribute.KeyValue, error) return []attribute.KeyValue{attribute.StringSlice(f.attributeName, reflectValueToSlice[string](fv))}, nil case reflect.Struct: if fv.Type().Elem().ConvertibleTo(timeType) { - // TODO: consider reimplemeting it with samber/lo.Map - times := reflectValueToSlice[time.Time](fv) - strs := make([]string, len(times)) - for i, t := range times { - strs[i] = t.Format(time.RFC3339) + strs := make([]string, fv.Len()) + for i := 0; i < fv.Len(); i++ { + t := fv.Index(i).Convert(timeType).Interface().(time.Time) + strs[i] = t.Format(time.RFC3339Nano) } return []attribute.KeyValue{attribute.StringSlice(f.attributeName, strs)}, nil } - fallthrough + fallthrough // OpenTelemetry プロトコルのレベルで、そもそも合成型の配列は非対応のため、いずれにせよ文字列化以外の選択肢は与えない default: bs, err := json.Marshal(fv.Interface()) if err != nil { diff --git a/pkg/otel/attr_test.go b/pkg/otel/attr_test.go index eb740a7..c9d7e10 100644 --- a/pkg/otel/attr_test.go +++ b/pkg/otel/attr_test.go @@ -5,8 +5,10 @@ import ( "fmt" "slices" "testing" + "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/attribute" ) @@ -49,6 +51,38 @@ func TestMarshalOtelAttributes__PrimitiveTypes(t *testing.T) { assertAttributes(t, want, got) } +func TestMarshalOtelAttributes__Time(t *testing.T) { + const layout = "2006-01-02T15:04:05.999999999" + const testTimeStr = "2021-01-01T00:00:00.123456789" + testTime, err := time.ParseInLocation(layout, testTimeStr, time.FixedZone("Asia/Tokyo", 9*60*60)) + require.NoError(t, err) + + type Wrapper time.Time + args := struct { + StdTime time.Time + WrapperTime Wrapper + Map map[string]interface{} + }{ + StdTime: testTime, + WrapperTime: Wrapper(testTime), + Map: map[string]interface{}{ + "std_time": testTime, + "wrapper_time": Wrapper(testTime), + }, + } + + const expectedStr = "2021-01-01T00:00:00.123456789+09:00" + want := []attribute.KeyValue{ + attribute.String("std_time", expectedStr), + attribute.String("wrapper_time", expectedStr), + attribute.String("map.std_time", expectedStr), + attribute.String("map.wrapper_time", expectedStr), + } + got, err := MarshalOtelAttributes(args) + assert.NoError(t, err) + assertAttributes(t, want, got) +} + type structWithNameTags struct { BoolValue bool `otel:"b"` BoolSlice []bool `otel:"bs"` diff --git a/pkg/otel/struct_field.go b/pkg/otel/struct_field.go index 7bca6cc..83e3129 100644 --- a/pkg/otel/struct_field.go +++ b/pkg/otel/struct_field.go @@ -33,11 +33,10 @@ func getStructFields(t reflect.Type) []structFiled { continue } attributeName := tagParts[0] - attributePrefix := tagParts[0] + "." if attributeName == "" { attributeName = camelToSnake(f.Name) - attributePrefix = "" } + attributePrefix := attributeName + "." var omitEmpty bool for _, part := range tagParts[1:] { if part == "omitempty" {