├── .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 | chatgptjoke -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------