├── .github
└── workflows
│ ├── go.yml
│ └── release.yml
├── .gitignore
├── .goreleaser.yml
├── LICENSE
├── README.md
├── ast.go
├── custom-log-marshaler.rb
├── fields.go
├── fixtures
├── helpers.go
├── zapexample.go
└── zerologexample.go
├── go.mod
├── go.sum
├── marshaler.go
├── zap.go
├── zap_test.go
├── zerolog.go
└── zerolog_test.go
/.github/workflows/go.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build a golang project
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
3 |
4 | name: Go
5 |
6 | on:
7 | push:
8 | branches: [ "master" ]
9 | pull_request:
10 | branches: [ "master" ]
11 |
12 | jobs:
13 |
14 | build:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v3
18 |
19 | - name: Set up Go
20 | uses: actions/setup-go@v3
21 | with:
22 | go-version: 1.19
23 |
24 | - name: Build
25 | run: go build -v ./...
26 |
27 | - name: Test
28 | run: go test -v ./...
29 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | # .github/workflows/release.yml
2 | name: Release
3 | on:
4 | push:
5 | tags:
6 | - 'v*'
7 |
8 | jobs:
9 | goreleaser:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Check out code
13 | uses: actions/checkout@v2
14 | with:
15 | fetch-depth: 0
16 |
17 | - name: Set up Go
18 | uses: actions/setup-go@v2
19 | with:
20 | go-version: 1.17
21 |
22 | - name: Cache dependencies
23 | uses: actions/cache@v2
24 | with:
25 | path: ~/go/pkg/mod
26 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
27 | restore-keys: |
28 | ${{ runner.os }}-go-
29 |
30 | - name: Run Goreleaser
31 | uses: goreleaser/goreleaser-action@v2
32 | with:
33 | version: latest
34 | args: release --rm-dist
35 | env:
36 | GITHUB_TOKEN: ${{ secrets.GH_PAT }}
37 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | pii-marshaler
--------------------------------------------------------------------------------
/.goreleaser.yml:
--------------------------------------------------------------------------------
1 | # .goreleaser.yml
2 | project_name: custom-log-marshaler
3 | builds:
4 | - dir: .
5 | env:
6 | - CGO_ENABLED=0
7 | goos:
8 | - linux
9 | - darwin
10 | goarch:
11 | - amd64
12 | - arm
13 | - arm64
14 | goarm:
15 | - "7"
16 | archives:
17 | - id: archive
18 | format_overrides:
19 | - goos: windows
20 | format: zip
21 | brews:
22 | - name: custom-log-marshaler
23 | tap:
24 | owner: solodynamo
25 | name: homebrew-tap
26 | token: "{{ .Env.GITHUB_TOKEN }}"
27 | folder: Formula
28 | homepage: https://github.com/solodynamo/custom-log-marshaler
29 | description: Attempt to R.I.P PII or unnecessary info in logs and reduce log ingestion costs in the process.
30 | license: MIT
31 | commit_author:
32 | name: Ankit Singh
33 | email: reachout@githubprofileblog
34 | skip_upload: false
35 | test: |
36 | system "#{bin}/custom-log-marshaler version"
37 | install: |
38 | bin.install "custom-log-marshaler"
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 solodynamo
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🕵️ Custom Log Marshaler
2 |
3 | "Don't log any data that is unnecessary or should not be logged in the first place." - common sense
4 |
5 | We can tag PII struct fields as "notloggable" and this generator will output custom marshal functions for those Golang structs which will prevent sending those fields to stdout!
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | ## How?
17 |
18 | In most Go logging libraries, there is a way to override the default marshaling function used to "log" to stdout. This package generates that custom function with some superpowers like excluding fields (for PII, not required stuff).
19 |
20 | If you send less data, ingestion bandwidth is used less so this also leads to lesser costs in the long run.
21 |
22 | Example:
23 | ```go
24 | type User struct {
25 | Name string `json:"name"`
26 | Email string `notloggable`
27 | Address string `json:"address", notloggable`
28 | }
29 |
30 | type UserDetailsResponse struct {
31 | User
32 | RequestID string `json:"rid"`
33 | FromCache bool `json:"fromCache"`
34 | Metadata []string `json:"md"`
35 | }
36 | // MarshalLogObject ...
37 | func (l User) MarshalLogObject(enc zapcore.ObjectEncoder) error {
38 | enc.AddString("name", l.Name) // not logging things which shouldn't be logged.
39 | return nil
40 | }
41 |
42 | // MarshalLogObject ...
43 | func (l UserDetailsResponse) MarshalLogObject(enc zapcore.ObjectEncoder) error {
44 | enc.AddObject("user", l.User)
45 | enc.AddString("request_id", l.RequestID)
46 | enc.AddBool("from_cache", l.FromCache)
47 | enc.AddArray("metadata", l.Metadata)
48 | return nil
49 | }
50 | ```
51 |
52 | See above example in action on [Go Playground](https://go.dev/play/p/cv_u168fm0e?v=goprev).
53 |
54 | # Why?
55 |
56 | Couldn't find something like this.
57 |
58 | # Installation
59 |
60 | MacOS
61 | ```bash
62 | brew tap solodynamo/homebrew-tap
63 | brew install custom-log-marshaler
64 | ```
65 |
66 | Others:
67 | ```bash
68 | go install github.com/solodynamo/custom-log-marshaler
69 | ```
70 |
71 | # Usage
72 |
73 | Zerolog
74 |
75 | ```bash
76 | custom-log-marshaler -f "path to go file" -lib zerolog
77 | ```
78 |
79 | Uber Zap(by default)
80 |
81 | ```bash
82 | custom-log-marshaler -f "path to go file"
83 |
84 | ```
85 |
86 | For bulk operation on multiple go files:
87 |
88 | ```bash
89 | curl -sSL https://gist.githubusercontent.com/solodynamo/b23de7cc6576179292871efc9b37e1f1/raw/apply-clm-go.sh | bash -s -- "path1/to/ignore" "path2/to/ignore"
90 |
91 | ```
92 | Note: Above bulk script handles the installation too but only for MacOS.
93 |
94 | # How to exclude PII?
95 | ```go
96 | type User struct {
97 | Email string `notloggable`
98 | }
99 | ```
100 | When custom struct tag `notloggable` is used for a field, that field is excluded from final logging irrespective of the lib.
101 |
102 | # Contributions
103 | Please do!
104 |
105 | # Just a joke
106 |
--------------------------------------------------------------------------------
/ast.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "go/ast"
5 | "go/token"
6 | "strings"
7 |
8 | "github.com/iancoleman/strcase"
9 | )
10 |
11 | const (
12 | cannotDetermine = "cannot_determine"
13 | customStruct = "custom_struct"
14 | customMap = "custom_map"
15 | customPrimitiveType = "custom_primitive_type"
16 | )
17 |
18 | var (
19 | primitiveTypes = []string{"bool", "byte", "complex64", "complex128", "error", "float32", "float64", "int", "int8", "int16", "int32", "int64", "rune", "string", "uint", "uint8", "uint16", "uint32", "uint64", "uintptr"}
20 | )
21 |
22 | func getFields(f *ast.File, loglib PIIMarshaler) ([]string, map[string][]field) {
23 | structs := make([]string, 0)
24 | structFields := make(map[string][]field)
25 |
26 | ast.Inspect(f, func(n ast.Node) bool {
27 | if v, ok := n.(*ast.GenDecl); ok {
28 | for _, s := range v.Specs {
29 | typeSpec, ok := s.(*ast.TypeSpec)
30 | if !ok {
31 | continue
32 | }
33 |
34 | structType, ok := typeSpec.Type.(*ast.StructType)
35 | if !ok {
36 | continue
37 | }
38 | structName := typeSpec.Name.String()
39 | structs = append(structs, structName)
40 | fields := make([]field, 0, len(structType.Fields.List))
41 |
42 | for i := 0; i < len(structType.Fields.List); i++ {
43 | fi := structType.Fields.List[i]
44 | fie := field{}
45 | if fi.Tag != nil && fi.Tag.Value != "" && strings.Contains(fi.Tag.Value, "notloggable") {
46 | continue
47 |
48 | }
49 |
50 | for _, ind := range fi.Names {
51 | fie.key = strcase.ToSnake(ind.Name)
52 | fie.fieldName = ind.Name
53 | }
54 |
55 | if len(fi.Names) == 0 {
56 | // This is an embedded field
57 | ident, ok := fi.Type.(*ast.Ident)
58 | if ok {
59 | fie.fieldName = ident.Name
60 | }
61 | }
62 |
63 | // Type
64 | if fi.Type != nil {
65 | fie.typeName = cannotDetermine
66 |
67 | if ty, ok := fi.Type.(*ast.Ident); ok {
68 | if ok, goType := isPrimitiveType(ty); ok {
69 | fie.typeName = goType
70 | goto register
71 | }
72 |
73 | if isCustomMap(f, ty.Name) {
74 | fie.typeName = customMap
75 | goto register
76 | }
77 |
78 | if ok, _ := isCustomPrimitive(f, ty.Name); ok {
79 | // force reflect custom defined primitive types
80 | fie.typeName = cannotDetermine
81 | goto register
82 | }
83 |
84 | if isCustomStruct(n, ty.Name) {
85 | fie.typeName = customStruct
86 | // make sure string in log fn doesn't cuse custom_struct string
87 | // uses the Actual field/struct name
88 | fie.key = ty.Name
89 | goto register
90 | }
91 |
92 | }
93 |
94 | if ty, ok := fi.Type.(*ast.StarExpr); ok {
95 | fie.fieldType = ptr
96 | if x, ok := ty.X.(*ast.Ident); ok {
97 | fie.typeName = x.Name
98 | }
99 |
100 | if cty, ok := ty.X.(*ast.SelectorExpr); ok {
101 | if x, ok := cty.X.(*ast.Ident); ok {
102 | fie.pkgName = x.Name
103 | }
104 |
105 | fie.typeName = cty.Sel.Name
106 | }
107 | goto register
108 | }
109 |
110 | if ty, ok := fi.Type.(*ast.SelectorExpr); ok {
111 | if x, ok := ty.X.(*ast.Ident); ok {
112 | fie.pkgName = x.Name
113 | }
114 |
115 | fie.typeName = ty.Sel.Name
116 | }
117 | }
118 | register:
119 | fie.libFunc = loglib.GetLibFunc(fie.allTypeName())
120 | fields = append(fields, fie)
121 | }
122 | structFields[structName] = fields
123 | }
124 | }
125 |
126 | return true
127 | })
128 |
129 | return structs, structFields
130 | }
131 |
132 | func isCustomStruct(n ast.Node, typeName string) bool {
133 | found := false
134 | switch x := n.(type) {
135 | case *ast.TypeSpec:
136 | if x.Name.Name == "User" {
137 | if _, ok := x.Type.(*ast.StructType); ok {
138 | found = true
139 | }
140 | }
141 | case *ast.StructType:
142 | for _, field := range x.Fields.List {
143 | if ident, ok := field.Type.(*ast.Ident); ok && ident.Name == "User" {
144 | found = true
145 | }
146 | }
147 | }
148 | return !found
149 | }
150 |
151 | func isCustomMap(f *ast.File, typeName string) bool {
152 | for _, decl := range f.Decls {
153 | genDecl, ok := decl.(*ast.GenDecl)
154 | if !ok || genDecl.Tok != token.TYPE {
155 | continue
156 | }
157 |
158 | for _, spec := range genDecl.Specs {
159 | typeSpec, ok := spec.(*ast.TypeSpec)
160 | if ok && typeSpec.Name.Name == typeName {
161 | _, isMap := typeSpec.Type.(*ast.MapType)
162 | return isMap
163 | }
164 | }
165 | }
166 |
167 | return false
168 | }
169 |
170 | func isCustomPrimitive(f *ast.File, typeName string) (bool, string) {
171 |
172 | for _, decl := range f.Decls {
173 | genDecl, ok := decl.(*ast.GenDecl)
174 | if !ok || genDecl.Tok != token.TYPE {
175 | continue
176 | }
177 |
178 | for _, spec := range genDecl.Specs {
179 | typeSpec, ok := spec.(*ast.TypeSpec)
180 | if ok && typeSpec.Name.Name == typeName {
181 | ident, isIdent := typeSpec.Type.(*ast.Ident)
182 | if isIdent {
183 | for _, primitiveType := range primitiveTypes {
184 | if ident.Name == primitiveType {
185 | return true, primitiveType
186 | }
187 | }
188 | }
189 | break
190 | }
191 | }
192 | }
193 |
194 | return false, ""
195 | }
196 |
197 | func isPrimitiveType(ident *ast.Ident) (bool, string) {
198 | for _, t := range primitiveTypes {
199 | if ident.Name == t {
200 | return true, t
201 | }
202 | }
203 |
204 | return false, ""
205 | }
206 |
--------------------------------------------------------------------------------
/custom-log-marshaler.rb:
--------------------------------------------------------------------------------
1 | # typed: false
2 | # frozen_string_literal: true
3 |
4 | # This file was generated by GoReleaser. DO NOT EDIT.
5 | class CustomLogMarshaler < Formula
6 | desc ""
7 | homepage ""
8 | version "1.0.3"
9 |
10 | on_macos do
11 | if Hardware::CPU.intel?
12 | url "https://github.com/solodynamo/custom-log-marshaler/releases/download/v1.0.3/custom-log-marshaler_1.0.3_darwin_amd64.tar.gz"
13 | sha256 "1d6ca4a4996dbff79c4fa904cff4fd282b8d9b4c6bf511e9dc8963679d033226"
14 |
15 | def install
16 | bin.install "custom-log-marshaler"
17 | end
18 | end
19 | if Hardware::CPU.arm?
20 | url "https://github.com/solodynamo/custom-log-marshaler/releases/download/v1.0.3/custom-log-marshaler_1.0.3_darwin_arm64.tar.gz"
21 | sha256 "9ee24c5b6839aa9973438c790cb5322b0b83c6030ec0e502d9b577936c2a9536"
22 |
23 | def install
24 | bin.install "custom-log-marshaler"
25 | end
26 | end
27 | end
28 |
29 | on_linux do
30 | if Hardware::CPU.arm? && Hardware::CPU.is_64_bit?
31 | url "https://github.com/solodynamo/custom-log-marshaler/releases/download/v1.0.3/custom-log-marshaler_1.0.3_linux_arm64.tar.gz"
32 | sha256 "0e7ea3f9c213e097695cc1f1b3544d805b068a713a78aee6c4194c80313b2085"
33 |
34 | def install
35 | bin.install "custom-log-marshaler"
36 | end
37 | end
38 | if Hardware::CPU.intel?
39 | url "https://github.com/solodynamo/custom-log-marshaler/releases/download/v1.0.3/custom-log-marshaler_1.0.3_linux_amd64.tar.gz"
40 | sha256 "5650997e06ceddf2ab5fc8d99af7416afe26c3916d99e1e8cb61971c5ab0095b"
41 |
42 | def install
43 | bin.install "custom-log-marshaler"
44 | end
45 | end
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/fields.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | // this code is highly inspired frrom https://github.com/muroon/zmlog, many thanks to the author.
4 | import (
5 | "fmt"
6 |
7 | "github.com/iancoleman/strcase"
8 | )
9 |
10 | type fieldType int
11 |
12 | const (
13 | normal fieldType = iota
14 | ptr
15 | slice
16 | )
17 |
18 | type field struct {
19 | key string
20 | fieldName string
21 | pkgName string
22 | typeName string
23 | fieldType fieldType
24 | libFunc string
25 | }
26 |
27 | func (f field) allTypeName() string {
28 | n := f.typeName
29 |
30 | if f.pkgName != "" {
31 | n = fmt.Sprintf("%s.%s", f.pkgName, f.typeName)
32 | }
33 |
34 | switch f.fieldType {
35 | case ptr:
36 | n = fmt.Sprintf("*%s", n)
37 | case slice:
38 | n = "[]"
39 | }
40 | return n
41 | }
42 |
43 | func (f field) isEmbedded() bool {
44 | return f.key == "" && f.allTypeName() != ""
45 | }
46 |
47 | func (f field) getKey() string {
48 | key := f.key
49 | return strcase.ToSnake(key)
50 | }
51 |
52 | func (f field) getFieldName() string {
53 | return f.fieldName
54 | }
55 |
56 | func (f field) ParamValue() string {
57 | fieldName := fmt.Sprintf("l.%s", f.getFieldName())
58 | if f.fieldType == ptr {
59 | fieldName = fmt.Sprintf("*l.%s", f.getFieldName())
60 | }
61 | str := fmt.Sprintf("%s(\"%s\", %s)", f.libFunc, f.getKey(), fieldName)
62 | return str
63 | }
64 |
65 | func (f field) FieldNameWithoutAestrix() string {
66 | fieldName := fmt.Sprintf("l.%s", f.getFieldName())
67 | return fieldName
68 | }
69 |
--------------------------------------------------------------------------------
/fixtures/helpers.go:
--------------------------------------------------------------------------------
1 | package fixtures
2 |
3 | import (
4 | "bufio"
5 | "fmt"
6 | "os"
7 | )
8 |
9 | func ContainsText(text string, fixturePath string) bool {
10 | // Open the file
11 | file, err := os.Open(fixturePath)
12 | if err != nil {
13 | return false
14 | }
15 | defer file.Close()
16 |
17 | // Create a scanner to read the file line by line
18 | scanner := bufio.NewScanner(file)
19 |
20 | // Loop through each line
21 | for scanner.Scan() {
22 | line := scanner.Text()
23 | if line == text {
24 | fmt.Println("Match found:", line)
25 | return true
26 | }
27 | }
28 |
29 | return false
30 | }
31 |
--------------------------------------------------------------------------------
/fixtures/zapexample.go:
--------------------------------------------------------------------------------
1 | package fixtures
2 |
3 | type User struct {
4 | Name *string `json:"name"`
5 | Email string `notloggable`
6 | Address string `json:"address", notloggable`
7 | }
8 |
9 | type Translation struct {
10 | Language string `json:"language,omitempty"`
11 | Translation string `json:"translation"`
12 | }
13 |
14 | type customType map[string]string
15 | type number int
16 |
17 | type UserDetailsResponse struct {
18 | User
19 | FromCache bool `json:"fromCache"`
20 | Translations []Translation `json:"translations"`
21 | Metadata customType `json:"metadata"`
22 | No number `json:"no"`
23 | }
24 |
--------------------------------------------------------------------------------
/fixtures/zerologexample.go:
--------------------------------------------------------------------------------
1 | package fixtures
2 |
3 | type User2 struct {
4 | Name string `json:"name"`
5 | Email string `notloggable`
6 | Address string `json:"address", notloggable`
7 | }
8 |
9 | type UserDetailsResponse2 struct {
10 | User
11 | RequestID string `json:"rid"`
12 | FromCache bool `json:"fromCache"`
13 | }
14 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/solodynamo/custom-log-marshaler
2 |
3 | go 1.19
4 |
5 | require (
6 | github.com/iancoleman/strcase v0.2.0
7 | github.com/stretchr/testify v1.8.2
8 | )
9 |
10 | require (
11 | github.com/kr/text v0.2.0 // indirect
12 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
13 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
14 | )
15 |
16 | require (
17 | github.com/davecgh/go-spew v1.1.1 // indirect
18 | github.com/pmezard/go-difflib v1.0.0 // indirect
19 | gopkg.in/yaml.v3 v3.0.1 // indirect
20 | )
21 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5 | github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0=
6 | github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
7 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
8 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
9 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
10 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
11 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
12 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
13 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
14 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
15 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
16 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
17 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
18 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
19 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
20 | github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
21 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
22 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
23 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
24 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
25 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
26 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
27 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
28 |
--------------------------------------------------------------------------------
/marshaler.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "go/parser"
7 | "go/token"
8 | "os"
9 | )
10 |
11 | type PIIMarshaler interface {
12 | Generate([]string, map[string][]field) string
13 | GetLibFunc(string) string
14 | }
15 |
16 | func generate(path string, loglib PIIMarshaler) {
17 | fset := token.NewFileSet()
18 | f, err := parser.ParseFile(fset, path, nil, parser.Mode(0))
19 | if err != nil {
20 | panic(err)
21 | }
22 |
23 | structs, structFields := getFields(f, loglib)
24 |
25 | if len(structs) == 0 {
26 | panic(fmt.Errorf("cannot generate no suitable struct exists target=%s", path))
27 | }
28 |
29 | data := loglib.Generate(structs, structFields)
30 | if data == "" {
31 | panic(fmt.Errorf("cannot generate file no suitable struct exists target=%s", path))
32 | }
33 |
34 | file, err := os.OpenFile(path, os.O_WRONLY|os.O_APPEND, 0644)
35 | if err != nil {
36 | panic(err)
37 | }
38 | defer file.Close()
39 |
40 | _, err = file.Write([]byte(data))
41 | if err != nil {
42 | panic(err)
43 | }
44 | }
45 |
46 | func main() {
47 | path := flag.String("f", "", "target file path")
48 | lib := flag.String("lib", "", "zap/zerolog?")
49 | flag.Parse()
50 |
51 | var loglib PIIMarshaler
52 | switch *lib {
53 | case "zerolog":
54 | loglib = &ZeroLog{}
55 | default:
56 | loglib = &UberZap{}
57 | }
58 | generate(*path, loglib)
59 | }
60 |
--------------------------------------------------------------------------------
/zap.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | )
7 |
8 | type UberZap struct {
9 | }
10 |
11 | var _ PIIMarshaler = &UberZap{}
12 |
13 | var (
14 | baseFormat = `// MarshalLogObject ...
15 | func (l %s) MarshalLogObject(enc zapcore.ObjectEncoder) error {
16 | %s
17 | }
18 | `
19 | ptrFieldFormat = "if %s != nil { enc.%s }"
20 | indent = "\t"
21 | newLine = "\n"
22 | )
23 |
24 | func (uz *UberZap) Generate(structs []string, structFields map[string][]field) string {
25 | if len(structs) == 0 {
26 | return ""
27 | }
28 |
29 | contents := make([]string, 0, len(structs))
30 | for _, st := range structs {
31 | content := uz.generateStructData(st, structFields)
32 | if content == "" {
33 | continue
34 | }
35 |
36 | contents = append(contents, content)
37 | }
38 | if len(contents) == 0 {
39 | return ""
40 | }
41 |
42 | return strings.Join(contents, "\n")
43 | }
44 |
45 | func (uz *UberZap) generateStructData(structName string, structFields map[string][]field) string {
46 | if len(structFields[structName]) == 0 {
47 | return ""
48 | }
49 |
50 | var (
51 | exe string
52 | val string
53 | targetFields = make([]string, 0, len(structFields[structName])+2)
54 | )
55 |
56 | for _, fie := range structFields[structName] {
57 | switch fie.fieldType {
58 | case ptr:
59 | exe = fmt.Sprintf(ptrFieldFormat, fie.FieldNameWithoutAestrix(), fie.ParamValue())
60 | val = fmt.Sprintf("%s%s%s%s", indent, indent, exe, newLine)
61 | default:
62 | exe = fmt.Sprintf("enc.%s", fie.ParamValue())
63 | val = fmt.Sprintf("%s%s%s%s", indent, indent, exe, newLine)
64 | }
65 | targetFields = append(targetFields, val)
66 | }
67 | suffix := fmt.Sprintf("%s%sreturn nil", indent, indent)
68 | targetFields = append(targetFields, suffix)
69 |
70 | return fmt.Sprintf(baseFormat, structName, strings.Join(targetFields, ""))
71 | }
72 |
73 | // https://github.com/uber-go/zap/blob/master/zapcore/encoder.go#L349
74 | func (uz *UberZap) GetLibFunc(typeName string) string {
75 | switch typeName {
76 | case "bool":
77 | return "AddBool"
78 | case "*bool":
79 | return "AddBool"
80 | case "complex128":
81 | return "AddComplex128"
82 | case "*complex128":
83 | return "AddComplex128"
84 | case "complex64":
85 | return "AddComplex64"
86 | case "*complex64":
87 | return "AddComplex64"
88 | case "float64":
89 | return "AddFloat64"
90 | case "*float64":
91 | return "AddFloat64"
92 | case "float32":
93 | return "AddFloat32"
94 | case "*float32":
95 | return "AddFloat32"
96 | case "int":
97 | return "AddInt"
98 | case "*int":
99 | return "AddInt"
100 | case "int64":
101 | return "AddInt64"
102 | case "*int64":
103 | return "AddInt64"
104 | case "int32":
105 | return "AddInt32"
106 | case "*int32":
107 | return "AddInt32"
108 | case "int16":
109 | return "AddInt16"
110 | case "*int16":
111 | return "AddInt16"
112 | case "int8":
113 | return "AddInt8"
114 | case "*int8":
115 | return "AddInt8"
116 | case "string":
117 | return "AddString"
118 | case "*string":
119 | return "AddString"
120 | case "uint":
121 | return "AddUint"
122 | case "*uint":
123 | return "AddUint"
124 | case "uint64":
125 | return "AddUint64"
126 | case "*uint64":
127 | return "AddUint64"
128 | case "uint32":
129 | return "AddUint32"
130 | case "*uint32":
131 | return "AddUint32"
132 | case "uint16":
133 | return "AddUint16"
134 | case "*uint16":
135 | return "AddUint16"
136 | case "uint8":
137 | return "AddUint8"
138 | case "*uint8":
139 | return "AddUint8"
140 | case "[]byte":
141 | return "AddBinary"
142 | case "uintptr":
143 | return "AddUintptr"
144 | case "*uintptr":
145 | return "AddUintptr"
146 | case "time.Time":
147 | return "AddTime"
148 | case "*time.Time":
149 | return "AddTime"
150 | case "time.Duration":
151 | return "AddDuration"
152 | case "*time.Duration":
153 | return "AddDuration"
154 | case "reflection":
155 | return "AddReflected"
156 | case "[]":
157 | return "AddArray"
158 | case customStruct:
159 | return "AddObject"
160 | default:
161 | return "AddReflected"
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/zap_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/solodynamo/custom-log-marshaler/fixtures"
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func TestZapGenerate(t *testing.T) {
11 | generate("./fixtures/zapexample.go", &UberZap{})
12 | assert.True(t, fixtures.ContainsText("\t\tif l.Name != nil { enc.AddString(\"name\", *l.Name) }", "./fixtures/zapexample.go"))
13 | assert.True(t, fixtures.ContainsText("\t\tenc.AddObject(\"user\", l.User)", "./fixtures/zapexample.go"))
14 | assert.True(t, fixtures.ContainsText("\t\tenc.AddBool(\"from_cache\", l.FromCache)", "./fixtures/zapexample.go"))
15 |
16 | assert.True(t, fixtures.ContainsText("\t\tenc.AddString(\"language\", l.Language)", "./fixtures/zapexample.go"))
17 | assert.True(t, fixtures.ContainsText("\t\tenc.AddString(\"translation\", l.Translation)", "./fixtures/zapexample.go"))
18 | assert.True(t, fixtures.ContainsText("\t\tenc.AddReflected(\"translations\", l.Translations)", "./fixtures/zapexample.go"))
19 | assert.True(t, fixtures.ContainsText("\t\tenc.AddReflected(\"metadata\", l.Metadata)", "./fixtures/zapexample.go"))
20 | assert.True(t, fixtures.ContainsText("\t\tenc.AddReflected(\"no\", l.No)", "./fixtures/zapexample.go"))
21 | }
22 |
--------------------------------------------------------------------------------
/zerolog.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | )
7 |
8 | type ZeroLog struct {
9 | }
10 |
11 | var _ PIIMarshaler = &ZeroLog{}
12 |
13 | func (zl *ZeroLog) Generate(structs []string, structFields map[string][]field) string {
14 | if len(structs) == 0 {
15 | return ""
16 | }
17 |
18 | contents := make([]string, 0, len(structs))
19 | for _, st := range structs {
20 | content := zl.generateStructData(st, structFields)
21 | if content == "" {
22 | continue
23 | }
24 |
25 | contents = append(contents, content)
26 | }
27 | if len(contents) == 0 {
28 | return ""
29 | }
30 |
31 | return strings.Join(contents, "\n")
32 | }
33 |
34 | func (zl *ZeroLog) generateStructData(structName string, structFields map[string][]field) string {
35 | if len(structFields[structName]) == 0 {
36 | return ""
37 | }
38 |
39 | var (
40 | exe string
41 | val string
42 | targetFields = make([]string, 0, len(structFields[structName])+2)
43 | )
44 |
45 | format := `// MarshalZerologObject ...
46 | func (l %s) MarshalZerologObject(enc *zerolog.Event) error {
47 | %s
48 | }
49 | `
50 | indent := "\t"
51 | newLine := "\n"
52 |
53 | for _, fie := range structFields[structName] {
54 | switch fie.fieldType {
55 | case ptr:
56 | exe = fmt.Sprintf(ptrFieldFormat, fie.FieldNameWithoutAestrix(), fie.ParamValue())
57 | val = fmt.Sprintf("%s%s%s%s", indent, indent, exe, newLine)
58 | default:
59 | exe = fmt.Sprintf("enc.%s", fie.ParamValue())
60 | val = fmt.Sprintf("%s%s%s%s", indent, indent, exe, newLine)
61 | }
62 | targetFields = append(targetFields, val)
63 | }
64 | suffix := fmt.Sprintf("%s%sreturn nil", indent, indent)
65 | targetFields = append(targetFields, suffix)
66 |
67 | return fmt.Sprintf(format, structName, strings.Join(targetFields, ""))
68 | }
69 |
70 | // https://github.com/rs/zerolog/blob/762546b5c64e03f3d23f867213e80aa45906aaf7/array.go
71 | func (zl *ZeroLog) GetLibFunc(typeName string) string {
72 | switch typeName {
73 | case "zerolog.LogObjectMarshaler":
74 | return "Object"
75 | case "bool":
76 | return "Bool"
77 | case "*bool":
78 | return "Bool"
79 | case "float64":
80 | return "AddFloat64"
81 | case "*float64":
82 | return "Float64"
83 | case "float32":
84 | return "Float32"
85 | case "*float32":
86 | return "Float32"
87 | case "int":
88 | return "Int"
89 | case "*int":
90 | return "Int"
91 | case "int64":
92 | return "Int64"
93 | case "*int64":
94 | return "Int64"
95 | case "int32":
96 | return "Int32"
97 | case "*int32":
98 | return "Int32"
99 | case "int16":
100 | return "Int16"
101 | case "*int16":
102 | return "Int16"
103 | case "int8":
104 | return "Int8"
105 | case "*int8":
106 | return "Int8"
107 | case "string":
108 | return "Str"
109 | case "*string":
110 | return "Str"
111 | case "uint":
112 | return "Uint"
113 | case "*uint":
114 | return "Uint"
115 | case "uint64":
116 | return "Uint64"
117 | case "*uint64":
118 | return "Uint64"
119 | case "uint32":
120 | return "Uint32"
121 | case "*uint32":
122 | return "Uint32"
123 | case "uint16":
124 | return "Uint16"
125 | case "*uint16":
126 | return "Uint16"
127 | case "uint8":
128 | return "Uint8"
129 | case "*uint8":
130 | return "Uint8"
131 | case "[]byte":
132 | return "Bytes"
133 | case "time.Time":
134 | return "Time"
135 | case "*time.Time":
136 | return "Time"
137 | case "time.Duration":
138 | return "Dur"
139 | case "*time.Duration":
140 | return "Dur"
141 | case customStruct:
142 | return "Object"
143 | default:
144 | return "Interface"
145 |
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/zerolog_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/solodynamo/custom-log-marshaler/fixtures"
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func TestZerologGenerate(t *testing.T) {
11 | generate("./fixtures/zerologexample.go", &ZeroLog{})
12 | assert.True(t, fixtures.ContainsText("\t\tenc.Object(\"user\", l.User)", "./fixtures/zerologexample.go"))
13 | assert.True(t, fixtures.ContainsText("\t\tenc.Str(\"request_id\", l.RequestID)", "./fixtures/zerologexample.go"))
14 | assert.True(t, fixtures.ContainsText("\t\tenc.Bool(\"from_cache\", l.FromCache)", "./fixtures/zerologexample.go"))
15 | }
16 |
--------------------------------------------------------------------------------