Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(gogenerate): add optional fields in log with omitempty annotation #619

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pkg/events/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ type HTTPRequest struct {
Endpoint string `log:"http.url_details.endpoint"`

// Route is the URL path without interpolating the path variables.
Route string `log:"http.route"`
Route string `log:"http.route,omitempty"`
}

// FillFieldsFromRequest fills in the standard request fields
Expand Down
57 changes: 46 additions & 11 deletions tools/logger/generating.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ Consider a `struct` like so:

```golang

type OrgInfo struct {
Org string
Guid string
type Org struct {
Shortname string
GUID string
}
```

Expand All @@ -20,37 +20,72 @@ this:

```golang

type OrgInfo struct
Org string `log:"or.org.shortname"`
Guid string `log:"or.org.guid"`
type Org struct
Shortname string `log:"or.org.shortname"`
GUID string `log:"or.org.guid"`
}

```

With the addition of the annotation (which matches the [standard
With the addition of the `log` tags for fields (which 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)
to generate the `MarshalLog` method:

```bash

# run from directory where the OrgInfo struct is stored
# run from directory where the Org struct is stored
$> go run github.com/getoutreach/gobox/tools/logger -output marshalers.go
$> cat marshalers.go
// Code generated by "logger -output marshalers.go"; DO NOT EDIT.

package log

func (s *OrgInfo) MarshalLog(addField func(key string, value interface{})) {
func (s *Org) MarshalLog(addField func(key string, value interface{})) {
if s == nil {
return
}
addField(or.org.shortname, Org)
addField(or.org.guid, Guid)
addField(or.org.shortname, Shortname)
addField(or.org.guid, GUID)
}
```

### Annotations
Supported annotations:

- `omitempty`: makes the field optional.

It is available for basic types, types aliased to a basic type,
and pointers of any type. The annotation parser does **not**
validate if the annotation is applied to supported types.

Example:

```go
type Org struct
Shortname string `log:"or.org.shortname,omitempty"`
GUID *Custom `log:"or.org.guid,omitempty"`
}
```

Generated code (post-formatted with `gofmt`):

```go
func (s *Org) MarshalLog(addField func(key string, value interface{})) {
if s == nil {
return
}
if s.Shortname != "" {
addField("or.org.shortname", s.Shortname)
}
if s.GUID != nil {
addField("or.org.guid", s.GUID)
}
}
```


## Using go generate

Go `generate` is the standard way to generate pre-build artifacts
Expand Down
71 changes: 59 additions & 12 deletions tools/logger/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}})`
optionalFieldFormat = `
if s.{{.name}} != %s {
addField("{{.key}}", s.{{.name}})
}`
nestedMarshalerFormat = `
s.{{.name}}.MarshalLog(addField)`
nestedNilableMarshalerFormat = `
Expand All @@ -54,6 +58,10 @@ if s.{{.name}} != nil {
}`
)

const (
annotationOmitEmpty = "omitempty"
)

func main() {
flag.Usage = usage

Expand Down Expand Up @@ -131,18 +139,29 @@ func filterStructs(pkg *packages.Package) ([]string, []*types.Struct) {
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 {
args := map[string]string{"key": field, "name": s.Field(kk).Name()}
switch {
case s.Field(kk).Type().String() == "time.Time":
write(w, timeFieldFormat, args)
case field == "." && isNilable(s.Field(kk).Type()):
write(w, nestedNilableMarshalerFormat, args)
case field == ".":
write(w, nestedMarshalerFormat, args)
default:
write(w, simpleFieldFormat, args)
}
field, ok := reflect.StructTag(s.Tag(kk)).Lookup("log")
if !ok {
continue
}

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":
write(w, timeFieldFormat, args)
case field == "." && isNilable(s.Field(kk).Type()):
write(w, nestedNilableMarshalerFormat, args)
case field == ".":
write(w, nestedMarshalerFormat, args)
case contains(annotations, annotationOmitEmpty):
write(w, getOptionalFieldFormat(s.Field(kk).Type()), args)
default:
write(w, simpleFieldFormat, args)
malept marked this conversation as resolved.
Show resolved Hide resolved
}
}
fmt.Fprintf(w, "\n}\n")
Expand All @@ -168,3 +187,31 @@ func usage() {
fmt.Fprintf(os.Stderr, "Flags:\n")
flag.PrintDefaults()
}

func getOptionalFieldFormat(p types.Type) string {
var defaultValue string
switch p.Underlying().String() {
case "string":
defaultValue = `""`
case "int", "int8", "int16", "int32", "int64",
"uint", "uint8", "uint16", "uint32", "uint64":
defaultValue = "0"
case "float32", "float64":
defaultValue = "0.0"
case "bool":
defaultValue = "false"
default:
defaultValue = "nil"
}

return fmt.Sprintf(optionalFieldFormat, defaultValue)
}

func contains[T comparable](slice []T, item T) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}
Loading
Loading