Skip to content

Commit

Permalink
feat(gogenerate): add optional fields in log with omitempty annotation (
Browse files Browse the repository at this point in the history
#619)

<!--
  !!!! README !!!! Please fill this out.

  Please follow conventional commit naming conventions:

  https://www.conventionalcommits.org/en/v1.0.0/#summary
-->

<!-- A short description of what your PR does and what it solves. -->
## What this PR does / why we need it

gobox cannot be re-stenciled due to manual change in autogenerated file
where a field was made optional. this PR adds `omitempty` annotation to
log tags, making it possible to mark fields optional.


<!-- <<Stencil::Block(jiraPrefix)>> -->

## Jira ID

[DT-4572](https://outreach-io.atlassian.net/browse/DT-4572)

<!-- <</Stencil::Block>> -->

<!-- Notes that may be helpful for anyone reviewing this PR -->
## Notes for your reviewers

<!-- <<Stencil::Block(custom)>> -->

<!-- <</Stencil::Block>> -->


[DT-4572]:
https://outreach-io.atlassian.net/browse/DT-4572?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

---------

Signed-off-by: T M Rezoan Tamal <[email protected]>
  • Loading branch information
iamrz1 authored Feb 13, 2025
1 parent 72886c6 commit 27d86a7
Show file tree
Hide file tree
Showing 4 changed files with 368 additions and 24 deletions.
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)
}
}
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

0 comments on commit 27d86a7

Please sign in to comment.