Skip to content

Commit

Permalink
Merge pull request #12 from ebi-yade/feature/time_rfc3339
Browse files Browse the repository at this point in the history
Stringify time.Time in format of RFC3339
  • Loading branch information
ebi-yade authored Nov 28, 2024
2 parents 1ef0e47 + 0156639 commit fc866f9
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 52 deletions.
26 changes: 13 additions & 13 deletions .github/workflows/reviewdog.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,19 @@ permissions:
issues: write

jobs:
golangci-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: golangci-lint
uses: reviewdog/action-golangci-lint@v2
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
level: warning
golangci_lint_flags: "--config=.golangci.yaml"
filter_mode: diff_context
reporter: github-pr-review
fail_on_error: true
# golangci-lint:
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v4
# - name: golangci-lint
# uses: reviewdog/action-golangci-lint@v2
# with:
# github_token: ${{ secrets.GITHUB_TOKEN }}
# level: warning
# golangci_lint_flags: "--config=.golangci.yaml"
# filter_mode: diff_context
# reporter: github-pr-review
# fail_on_error: true

actionlint:
runs-on: ubuntu-latest
Expand Down
57 changes: 44 additions & 13 deletions pkg/otel/attr.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"reflect"
"time"

"go.opentelemetry.io/otel/attribute"
)
Expand Down Expand Up @@ -41,7 +42,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 {
Expand Down Expand Up @@ -145,6 +146,12 @@ func marshalMap(rv reflect.Value) ([]attribute.KeyValue, error) {
case []string:
attrs = append(attrs, attribute.StringSlice(keyString, value))
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,
Expand Down Expand Up @@ -192,19 +199,22 @@ func marshalField(f structFiled, fv reflect.Value) ([]attribute.KeyValue, error)
case reflect.Slice, reflect.Array:
return marshalSlice(f, fv)
case reflect.Struct:
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
// convert time.Time to string
if fv.Type().ConvertibleTo(timeType) {
t := fv.Convert(timeType).Interface().(time.Time)
return []attribute.KeyValue{attribute.String(f.attributeName, t.Format(time.RFC3339Nano))}, nil
}
return kvs, nil
return f.marshalNested(fv)

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 {
Expand All @@ -214,6 +224,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:
Expand All @@ -226,13 +247,23 @@ func marshalSlice(f structFiled, fv reflect.Value) ([]attribute.KeyValue, error)
return []attribute.KeyValue{attribute.Float64Slice(f.attributeName, reflectValueToSlice[float64](fv))}, nil
case reflect.String:
return []attribute.KeyValue{attribute.StringSlice(f.attributeName, reflectValueToSlice[string](fv))}, nil
default:
bs, err := json.Marshal(fv.Interface())
if err != nil {
return []attribute.KeyValue{}, err
case reflect.Struct:
if fv.Type().Elem().ConvertibleTo(timeType) {
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
}
return []attribute.KeyValue{attribute.String(f.attributeName, string(bs))}, nil
// There is no choice but to provide only stringification because composite arrays are not supported at the OpenTelemetry protocol level.
}
bs, err := json.Marshal(fv.Interface())
if err != nil {
return []attribute.KeyValue{}, err
}

return []attribute.KeyValue{attribute.String(f.attributeName, string(bs))}, nil
}

func reflectValueToSlice[T any](v reflect.Value) []T {
Expand Down
70 changes: 48 additions & 22 deletions pkg/otel/attr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import (
"fmt"
"slices"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/attribute"
)

Expand Down Expand Up @@ -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"`
Expand Down Expand Up @@ -139,11 +173,7 @@ func TestMarshalOtelAttributes__WithStructPointerMarshaller(t *testing.T) {
}

func TestMarshalOtelAttributes__NestedStructWithMarshallerMember(t *testing.T) {
args := struct {
Struct structWithMarshaller
}{
Struct: structWithMarshaller{Value: 200},
}
args := structWithMarshaller{Value: 200}
want := []attribute.KeyValue{
attribute.Int("http.staus_code", 200), // `struct.` prefix is omitted
}
Expand Down Expand Up @@ -178,28 +208,24 @@ func TestMarshalOtelAttributes__WithStructInStruct(t *testing.T) {
},
}
want := []attribute.KeyValue{
attribute.BoolSlice("bs", []bool{true}),
attribute.IntSlice("is", []int{1}),
attribute.Int64Slice("is64", []int64{2}),
attribute.Float64Slice("fs", []float64{3.14, 2.71}),
attribute.StringSlice("ss", []string{"hello", "world"}),
attribute.BoolSlice("struct.bs", []bool{true}),
attribute.IntSlice("struct.is", []int{1}),
attribute.Int64Slice("struct.is64", []int64{2}),
attribute.Float64Slice("struct.fs", []float64{3.14, 2.71}),
attribute.StringSlice("struct.ss", []string{"hello", "world"}),
}
got, err := MarshalOtelAttributes(args)
assert.NoError(t, err)
assertAttributes(t, want, got)
}

func TestMarshalOtelAttributes__WithStructInStructPointer(t *testing.T) {
args := struct {
Struct *structWithNameAndOmitemptyTags
}{
Struct: &structWithNameAndOmitemptyTags{
BoolSlice: []bool{true},
IntSlice: []int{1},
Int64Slice: []int64{2},
FloatSlice: []float64{3.14, 2.71},
StringSlice: []string{"hello", "world"},
},
args := &structWithNameAndOmitemptyTags{
BoolSlice: []bool{true},
IntSlice: []int{1},
Int64Slice: []int64{2},
FloatSlice: []float64{3.14, 2.71},
StringSlice: []string{"hello", "world"},
}
want := []attribute.KeyValue{
attribute.BoolSlice("bs", []bool{true}),
Expand All @@ -213,11 +239,11 @@ func TestMarshalOtelAttributes__WithStructInStructPointer(t *testing.T) {
assertAttributes(t, want, got)
}

func TestMarshalOtelAttributes__WithStructInStructWithPrefix(t *testing.T) {
func TestMarshalOtelAttributes__WithStructInStructWithPrefixTag(t *testing.T) {
args := struct {
Struct structWithNameAndOmitemptyTags `otel:"test"`
}{
Struct: structWithNameAndOmitemptyTags{
structWithNameAndOmitemptyTags{
BoolSlice: []bool{true},
IntSlice: []int{1},
Int64Slice: []int64{2},
Expand Down
3 changes: 3 additions & 0 deletions pkg/otel/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package otel
import (
"reflect"
"sync"
"time"
)

type cache[T any] struct {
Expand All @@ -28,3 +29,5 @@ func (c *cache[T]) set(t reflect.Type, v T) {
c.cache[t] = v
c.mu.Unlock()
}

var timeType = reflect.TypeOf(time.Time{})
3 changes: 1 addition & 2 deletions pkg/otel/struct_field.go
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand Down
3 changes: 1 addition & 2 deletions spans.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ import (
"fmt"
"log/slog"

pkgotel "github.com/ebi-yade/spans/pkg/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"

pkgotel "github.com/ebi-yade/spans/pkg/otel"
)

type KeyValue struct {
Expand Down

0 comments on commit fc866f9

Please sign in to comment.