diff --git a/tools/logger/generating.md b/tools/logger/generating.md index acf5e482..6b888995 100644 --- a/tools/logger/generating.md +++ b/tools/logger/generating.md @@ -27,7 +27,7 @@ type OrgInfo struct ``` -With the addition of the annotation (which matches the [standard +With the addition of the log tags `ie. log:"xyz"` for fields (xyz matches the [standard attributes](https://app.datadoghq.com/logs/pipelines/standard-attributes)), we can use [logger](https://github.com/getoutreach/gobox/tree/master/tools/logger) @@ -51,6 +51,41 @@ func (s *OrgInfo) MarshalLog(addField func(key string, value interface{})) { } ``` +### Annotations: +These tags for fields also have limited support for annotations: `ie. log:"xyz,annotation"`. + +Supported annotations: + +- **omitempty**: makes the field optional. + + `omitempty` annotation is available for simple built-in types or + custom types with underlying type being built-in or pointers of any type. + + Example: + ```go + type OrgInfo struct + Org string `log:"or.org.shortname,omitempty"` + Guid *Custom `log:"or.org.guid,omitempty"` + } + ``` + + Generated code: + + ```go + func (s *OrgInfo) MarshalLog(addField func(key string, value interface{})) { + if s == nil { + return + } + if s.Org != "" { + addField("or.org.shortname", s.Org) + } + if s.Guid != nil { + addField("or.org.guid", s.Guid) + } + } + ``` + + ## Using go generate Go `generate` is the standard way to generate pre-build artifacts diff --git a/tools/logger/logger.go b/tools/logger/logger.go index a84884cb..63e81649 100644 --- a/tools/logger/logger.go +++ b/tools/logger/logger.go @@ -46,6 +46,10 @@ func (s *{{ .name }}) MarshalLog(addField func(key string, value interface{})) { addField("{{.key}}", s.{{.name}}.UTC().Format(time.RFC3339Nano))` simpleFieldFormat = ` addField("{{.key}}", s.{{.name}})` + simpleOptionalFieldFormat = ` +if s.{{.name}} != %s { + addField("{{.key}}", s.{{.name}}) +}` nestedMarshalerFormat = ` s.{{.name}}.MarshalLog(addField)` nestedNilableMarshalerFormat = ` @@ -55,7 +59,7 @@ if s.{{.name}} != nil { ) const ( - tagOmitEmpty = ",omitempty" + annotationOmitEmpty = "omitempty" ) func main() { @@ -136,6 +140,12 @@ func processStruct(w io.Writer, s *types.Struct, name string) { write(w, functionHeaderFormat, map[string]string{"name": name}) for kk := 0; kk < s.NumFields(); kk++ { if field, ok := reflect.StructTag(s.Tag(kk)).Lookup("log"); ok { + var annotations string + fieldParts := strings.SplitN(field, ",", 2) + field = fieldParts[0] + if len(fieldParts) > 1 { + annotations = fieldParts[1] + } args := map[string]string{"key": field, "name": s.Field(kk).Name()} switch { case s.Field(kk).Type().String() == "time.Time": @@ -144,8 +154,7 @@ func processStruct(w io.Writer, s *types.Struct, name string) { write(w, nestedNilableMarshalerFormat, args) case field == ".": write(w, nestedMarshalerFormat, args) - case strings.HasSuffix(field, tagOmitEmpty): - args["key"] = strings.TrimSuffix(field, tagOmitEmpty) + case strings.Contains(annotations, annotationOmitEmpty): write(w, getSimpleOptionalFieldFormat(s.Field(kk).Type()), args) default: write(w, simpleFieldFormat, args) @@ -192,8 +201,5 @@ func getSimpleOptionalFieldFormat(p types.Type) string { defaultValue = "nil" } - return fmt.Sprintf(` -if s.{{.name}} != %s { - addField("{{.key}}", s.{{.name}}) -}`, defaultValue) + return fmt.Sprintf(simpleOptionalFieldFormat, defaultValue) } diff --git a/tools/logger/logger_test.go b/tools/logger/logger_test.go index b3f52ad6..473f7400 100644 --- a/tools/logger/logger_test.go +++ b/tools/logger/logger_test.go @@ -1,7 +1,9 @@ package main import ( + "bytes" "go/types" + "gotest.tools/v3/assert" "testing" ) @@ -80,3 +82,121 @@ func TestGetSimpleOptionalFieldFormat(t *testing.T) { }) } } + +func TestProcessStruct(t *testing.T) { + tests := []struct { + name string + setup func() (*types.Struct, string) + expected string + }{ + { + name: "basic fields", + setup: func() (*types.Struct, string) { + fields := []*types.Var{ + types.NewField(0, nil, "Name", types.Typ[types.String], false), + types.NewField(0, nil, "Age", types.Typ[types.Int], false), + } + tags := []string{ + `log:"name"`, + `log:"age"`, + } + return types.NewStruct(fields, tags), "BasicStruct" + }, + expected: ` +func (s *BasicStruct) MarshalLog(addField func(key string, value interface{})) { + if s == nil { + return + } + +addField("name", s.Name) +addField("age", s.Age) +} +`, + }, + { + name: "time field", + setup: func() (*types.Struct, string) { + pkg := types.NewPackage("time", "time") + timeType := types.NewNamed(types.NewTypeName(0, pkg, "Time", nil), &types.Struct{}, nil) + + fields := []*types.Var{ + types.NewField(0, nil, "CreatedAt", timeType, false), + } + tags := []string{`log:"created_at"`} + return types.NewStruct(fields, tags), "TimeStruct" + }, + expected: ` +func (s *TimeStruct) MarshalLog(addField func(key string, value interface{})) { + if s == nil { + return + } + +addField("created_at", s.CreatedAt.UTC().Format(time.RFC3339Nano)) +} +`, + }, + { + name: "omitempty fields", + setup: func() (*types.Struct, string) { + fields := []*types.Var{ + types.NewField(0, nil, "Name", types.Typ[types.String], false), + types.NewField(0, nil, "AgeP", types.NewPointer(types.Typ[types.Int]), false), + } + tags := []string{ + `log:"name,omitempty"`, + `log:"ageP,omitempty"`, + } + return types.NewStruct(fields, tags), "OmitStruct" + }, + expected: ` +func (s *OmitStruct) MarshalLog(addField func(key string, value interface{})) { + if s == nil { + return + } + +if s.Name != "" { + addField("name", s.Name) +} +if s.AgeP != nil { + addField("ageP", s.AgeP) +} +} +`, + }, + { + name: "nested marshaler", + setup: func() (*types.Struct, string) { + nestedType := types.NewPointer(types.NewNamed( + types.NewTypeName(0, nil, "NestedStruct", nil), + &types.Struct{}, + nil, + )) + fields := []*types.Var{ + types.NewField(0, nil, "Nested", nestedType, false), + } + tags := []string{`log:"."`} + return types.NewStruct(fields, tags), "ParentStruct" + }, + expected: ` +func (s *ParentStruct) MarshalLog(addField func(key string, value interface{})) { + if s == nil { + return + } + +if s.Nested != nil { + s.Nested.MarshalLog(addField) +} +} +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, name := tt.setup() + buf := &bytes.Buffer{} + processStruct(buf, s, name) + assert.Equal(t, tt.expected, buf.String()) + }) + } +}