diff --git a/cmd/root.go b/cmd/root.go index 93624186..23e3ee4c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,17 +1,15 @@ package cmd import ( - "fmt" "os" - "path/filepath" - "runtime/debug" - "time" "github.com/knadh/koanf/providers/posflag" "github.com/knadh/koanf/v2" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/spf13/cobra" + + "github.com/theopenlane/core/pkg/logx/consolelog" ) const appName = "openlane" @@ -60,11 +58,10 @@ func initCmdFlags(cmd *cobra.Command) error { } func setupLogging() { - // setup logging with time and app name + // if you want to try the other console writer, swap this out for pzlog.NewPtermWriter() + output := consolelog.NewConsoleWriter() log.Logger = zerolog.New(os.Stderr). With().Timestamp(). - Logger(). - With().Str("app", appName). Logger() // set the log level @@ -74,22 +71,12 @@ func setupLogging() { if k.Bool("debug") { zerolog.SetGlobalLevel(zerolog.DebugLevel) - buildInfo, _ := debug.ReadBuildInfo() - log.Logger = log.Logger.With(). - Caller(). - Int("pid", os.Getpid()). - Str("go_version", buildInfo.GoVersion).Logger() + Caller().Logger() } // pretty logging for development if k.Bool("pretty") { - log.Logger = log.Output(zerolog.ConsoleWriter{ - Out: os.Stderr, - TimeFormat: time.RFC3339, - FormatCaller: func(i interface{}) string { - return filepath.Base(fmt.Sprintf("%s", i)) - }, - }) + log.Logger = log.Output(output) } } diff --git a/go.mod b/go.mod index 9cc04d56..3f9e717d 100644 --- a/go.mod +++ b/go.mod @@ -86,6 +86,9 @@ require ( require ( 4d63.com/gocheckcompilerdirectives v1.2.1 // indirect 4d63.com/gochecknoglobals v0.2.1 // indirect + atomicgo.dev/cursor v0.2.0 // indirect + atomicgo.dev/keyboard v0.2.9 // indirect + atomicgo.dev/schedule v0.1.0 // indirect cel.dev/expr v0.18.0 // indirect github.com/4meepo/tagalign v1.3.4 // indirect github.com/Abirdcfly/dupword v0.1.3 // indirect @@ -134,6 +137,7 @@ require ( github.com/chavacava/garif v0.1.0 // indirect github.com/cheekybits/genny v1.0.0 // indirect github.com/ckaznocha/intrange v0.3.0 // indirect + github.com/containerd/console v1.0.3 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect github.com/cpuguy83/dockercfg v0.3.2 // indirect @@ -201,6 +205,7 @@ require ( github.com/ldez/grignotin v0.6.0 // indirect github.com/ldez/tagliatelle v0.7.1 // indirect github.com/leonklingele/grouper v1.1.2 // indirect + github.com/lithammer/fuzzysearch v1.1.8 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/macabu/inamedparam v0.1.3 // indirect github.com/maratori/testableexamples v1.0.0 // indirect @@ -264,6 +269,7 @@ require ( github.com/uudashr/gocognit v1.2.0 // indirect github.com/uudashr/iface v1.3.0 // indirect github.com/xen0n/gosmopolitan v1.2.2 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yagipy/maintidx v1.0.0 // indirect github.com/yeya24/promlinter v0.3.0 // indirect github.com/ykadowak/zerologlint v0.1.5 // indirect @@ -336,7 +342,7 @@ require ( github.com/dvsekhvalnov/jose2go v1.7.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect - github.com/fatih/color v1.18.0 // indirect + github.com/fatih/color v1.18.0 github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect @@ -352,7 +358,7 @@ require ( github.com/go-openapi/swag v0.23.0 // indirect github.com/go-redis/redis/v8 v8.11.5 // indirect github.com/go-webauthn/x v0.1.14 // indirect - github.com/goccy/go-json v0.10.3 // indirect + github.com/goccy/go-json v0.10.3 github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/cel-go v0.22.1 // indirect @@ -362,6 +368,7 @@ require ( github.com/google/s2a-go v0.1.8 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect github.com/googleapis/gax-go/v2 v2.14.0 // indirect + github.com/gookit/color v1.5.4 github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/securecookie v1.1.2 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect @@ -416,6 +423,7 @@ require ( github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.61.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect + github.com/pterm/pterm v0.12.80 github.com/resend/resend-go/v2 v2.11.0 // indirect github.com/riverqueue/river/riverdriver v0.14.2 // indirect github.com/riverqueue/river/rivershared v0.14.2 // indirect diff --git a/go.sum b/go.sum index dd17ab80..4543ab74 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,14 @@ ariga.io/atlas v0.26.1 h1:UwLn9sXgcuoo9/A3sxXhDqnOImXUcaYb2JqVP0FQciw= ariga.io/atlas v0.26.1/go.mod h1:KPLc7Zj+nzoXfWshrcY1RwlOh94dsATQEy4UPrF2RkM= ariga.io/entcache v0.1.0 h1:nfJXzjB5CEvAK6SmjupHREMJrKLakeqU5tG3s4TO6JA= ariga.io/entcache v0.1.0/go.mod h1:3Z1Sql5bcqPA1YV/jvMlZyh9T+ntSFOclaASAm1TiKQ= +atomicgo.dev/assert v0.0.2 h1:FiKeMiZSgRrZsPo9qn/7vmr7mCsh5SZyXY4YGYiYwrg= +atomicgo.dev/assert v0.0.2/go.mod h1:ut4NcI3QDdJtlmAxQULOmA13Gz6e2DWbSAS8RUOmNYQ= +atomicgo.dev/cursor v0.2.0 h1:H6XN5alUJ52FZZUkI7AlJbUc1aW38GWZalpYRPpoPOw= +atomicgo.dev/cursor v0.2.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU= +atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8= +atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= +atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs= +atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU= cel.dev/expr v0.18.0 h1:CJ6drgk+Hf96lkLikr4rFf19WrU0BOWEihyZnI2TAzo= cel.dev/expr v0.18.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= @@ -60,6 +68,15 @@ github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 h1:sHglBQTwgx+rW github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.0 h1:/fTUt5vmbkAcMBt4YQiuC23cV0kEsN1MVMNqeOW43cU= github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.0/go.mod h1:ONJg5sxcbsdQQ4pOW8TGdTidT2TMAUy/2Xhr8mrYaao= +github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= +github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= +github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= +github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k= +github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI= +github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c= +github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE= +github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4= +github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY= github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= @@ -110,6 +127,7 @@ github.com/ashanbrown/forbidigo v1.6.0 h1:D3aewfM37Yb3pxHujIPSpTf6oQk9sc9WZi8ger github.com/ashanbrown/forbidigo v1.6.0/go.mod h1:Y8j9jy9ZYAEHXdu723cUlraTqbzjKF1MUyfOKL+AjcU= github.com/ashanbrown/makezero v1.2.0 h1:/2Lp1bypdmK9wDIq7uWBlDF1iMUpIIS4A+pF6C9IEUU= github.com/ashanbrown/makezero v1.2.0/go.mod h1:dxlPhHbDMC6N6xICzFBSK+4njQDdK8euNO0qjQMtGY4= +github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= github.com/aws/aws-sdk-go-v2 v1.32.6 h1:7BokKRgRPuGmKkFMhEg/jSul+tB9VvXhcViILtfG8b4= github.com/aws/aws-sdk-go-v2 v1.32.6/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 h1:lL7IfaFzngfx0ZwUGOZdsFFnQ5uLvR0hWqqhyE7Q9M8= @@ -215,6 +233,8 @@ github.com/ckaznocha/intrange v0.3.0 h1:VqnxtK32pxgkhJgYQEeOArVidIPg+ahLP7WBOXZd github.com/ckaznocha/intrange v0.3.0/go.mod h1:+I/o2d2A1FBHgGELbGxzIcyd3/9l9DuwjM8FsbSS3Lo= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8= github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= @@ -460,6 +480,10 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gT github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= github.com/googleapis/gax-go/v2 v2.14.0 h1:f+jMrjBPl+DL9nI4IQzLUxMq7XrAqFYB7hBPqMNIe8o= github.com/googleapis/gax-go/v2 v2.14.0/go.mod h1:lhBCnjdLrWRaPvLWhmc8IS24m9mr07qSYnHncrgo+zk= +github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= +github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= +github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= +github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= github.com/gordonklaus/ineffassign v0.1.0 h1:y2Gd/9I7MdY1oEIt+n+rowjBNDcLQq3RsH5hwJd0f9s= github.com/gordonklaus/ineffassign v0.1.0/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= @@ -557,6 +581,9 @@ github.com/kkHAIKE/contextcheck v1.1.5 h1:CdnJh63tcDe53vG+RebdpdXJTc9atMgGqdx8LX github.com/kkHAIKE/contextcheck v1.1.5/go.mod h1:O930cpht4xb1YQpK+1+AgoM3mFsvxr7uyFptcnWTYUA= github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0= github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= @@ -610,6 +637,8 @@ github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNB github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= +github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/macabu/inamedparam v0.1.3 h1:2tk/phHkMlEL/1GNe/Yf6kkR/hkcUdAEY3L0hjYV1Mk= @@ -638,6 +667,7 @@ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= @@ -773,6 +803,15 @@ github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFS github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= +github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg= +github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE= +github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU= +github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE= +github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8= +github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= +github.com/pterm/pterm v0.12.80 h1:mM55B+GnKUnLMUSqhdINe4s6tOuVQIetQ3my8JGyAIg= +github.com/pterm/pterm v0.12.80/go.mod h1:c6DeF9bSnOSeFPZlfs4ZRAFcf5SCoTwvwQ5xaKGQlHo= github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 h1:+Wl/0aFp0hpuHM3H//KMft64WQ1yX9LdJY64Qm/gFCo= github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1/go.mod h1:GJLgqsLeo4qgavUoL8JeGFNS7qcisx3awV/w9eWTmNI= github.com/quasilyte/go-ruleguard/dsl v0.3.22 h1:wd8zkOhSNr+I+8Qeciml08ivDt1pSXe60+5DqOpCjPE= @@ -845,6 +884,7 @@ github.com/securego/gosec/v2 v2.21.4/go.mod h1:Jtb/MwRQfRxCXyCm1rfM1BEiiiTfUOdyz github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= @@ -997,6 +1037,9 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xen0n/gosmopolitan v1.2.2 h1:/p2KTnMzwRexIW8GlKawsTWOxn7UHA+jCMF/V8HHtvU= github.com/xen0n/gosmopolitan v1.2.2/go.mod h1:7XX7Mj61uLYrj0qmeN0zi7XDon9JRAEhYQqAPLVNTeg= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM= @@ -1171,9 +1214,11 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1196,6 +1241,8 @@ golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -1323,9 +1370,11 @@ gopkg.in/mattn/go-runewidth.v0 v0.0.4/go.mod h1:BmXejnxvhwdaATwiJbB1vZ2dtXkQKZGu gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= diff --git a/pkg/logx/consolelog/README.md b/pkg/logx/consolelog/README.md new file mode 100644 index 00000000..03191c4d --- /dev/null +++ b/pkg/logx/consolelog/README.md @@ -0,0 +1,65 @@ +# Console writer for Zerolog + +This is a wrapper around zerolog's console writer to provide better readability when printing logs to the console. The package doesn't come with many colorize options or field formatting but it does provide interfaces to modify them, which is what this package does. + +At some point in any application there's going to be various preferences and needs in terms of log output and debugging, so the hope is that this package allows for easier extension of the underlying logger. + +You can consume it similar to below: + +```go +package main + +import ( + "github.com/rs/zerolog" + + "github.com/theopenlane/core/pkg/logx/consolelog" +) + +func main() { + output := consolelog.NewConsoleWriter() + logger := zerolog.New(output).With().Timestamp().Logger() + + logger.Info().Str("foo", "bar").Msg("Hello world") + + // => 3:50PM INF Hello world foo=bar +} +``` + +### Custom configuration + +```go +package main + +import ( + "fmt" + "strings" + "time" + + "github.com/rs/zerolog" + + "github.com/theopenlane/core/pkg/logx/consolelog" +) + +func main() { + output := consolelog.NewConsoleWriter( + // Customize time formatting + // + func(w *consolelog.ConsoleWriter) { + w.TimeFormat = time.Stamp + }, + // Customize "level" formatting + // + func(w *consolelog.ConsoleWriter) { + w.SetFormatter( + zerolog.LevelFieldName, + func(i interface{}) string { return strings.ToUpper(fmt.Sprintf("%-5s", i)) }) + }, + ) + + logger := zerolog.New(output).With().Timestamp().Logger() + + logger.Info().Str("foo", "bar").Msg("Hello world") + + // => Jul 19 15:50:00 INFO Hello world foo=bar +} +``` \ No newline at end of file diff --git a/pkg/logx/consolelog/consolelog.go b/pkg/logx/consolelog/consolelog.go new file mode 100644 index 00000000..b0d313cf --- /dev/null +++ b/pkg/logx/consolelog/consolelog.go @@ -0,0 +1,259 @@ +package consolelog + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + "sort" + "strings" + "time" + + "github.com/fatih/color" + "github.com/rs/zerolog" +) + +const ( + defaultTimeFormat = time.Kitchen +) + +var ( + bold = color.New(color.Bold).SprintFunc() + red = color.New(color.FgRed).SprintFunc() + green = color.New(color.FgGreen).SprintFunc() + yellow = color.New(color.FgYellow).SprintFunc() + faint = color.New(color.Faint).SprintFunc() + blue = color.New(color.FgCyan).SprintFunc() + + defaultFormatter = func(i interface{}) string { return fmt.Sprintf("%s", i) } + defaultPartsOrder = []string{ + zerolog.TimestampFieldName, + zerolog.LevelFieldName, + zerolog.CallerFieldName, + zerolog.MessageFieldName, + } +) + +// ConsoleWriter parses the JSON input and writes an ANSI-colorized output to out +type ConsoleWriter struct { + Out io.Writer + TimeFormat string + PartsOrder []string + formatters map[string]Formatter + FieldsExclude []string +} + +// Formatter transforms the input into a string +type Formatter func(interface{}) string + +type event map[string]interface{} + +// NewConsoleWriter creates and initializes a new ConsoleWriter +func NewConsoleWriter(options ...func(w *ConsoleWriter)) ConsoleWriter { + w := ConsoleWriter{Out: os.Stdout, TimeFormat: defaultTimeFormat, PartsOrder: defaultPartsOrder} + w.formatters = make(map[string]Formatter) + + w.setDefaultFormatters() + + for _, opt := range options { + opt(&w) + } + + return w +} + +// Formatter returns a formatter by id or the default formatter if none is found +func (w ConsoleWriter) Formatter(id string) Formatter { + if f, ok := w.formatters[id]; ok { + return f + } + + return defaultFormatter +} + +// SetFormatter registers a formatter function by id +func (w ConsoleWriter) SetFormatter(id string, f Formatter) { + w.formatters[id] = f +} + +// Write appends the output to Out. +func (w ConsoleWriter) Write(p []byte) (n int, err error) { + var buf bytes.Buffer + + var evt event + + d := json.NewDecoder(bytes.NewReader(p)) + d.UseNumber() + + err = d.Decode(&evt) + if err != nil { + return n, err + } + + for _, p := range w.PartsOrder { + w.writePart(&buf, evt, p) + } + + w.writeFields(evt, &buf) + + buf.WriteByte('\n') + + if _, err := buf.WriteTo(w.Out); err != nil { + return len(p), err + } + + return len(p), nil +} + +func (w ConsoleWriter) writePart(buf *bytes.Buffer, evt event, p string) { + var s = w.Formatter(p)(evt[p]) + if len(s) > 0 { + buf.WriteString(s) + + if p != w.PartsOrder[len(w.PartsOrder)-1] { + buf.WriteByte(' ') + } + } +} + +func (w ConsoleWriter) writeFields(evt event, buf *bytes.Buffer) { + var fields = make([]string, 0, len(evt)) + + for field := range evt { + switch field { + case zerolog.LevelFieldName, zerolog.TimestampFieldName, zerolog.MessageFieldName, zerolog.CallerFieldName: + continue + } + + fields = append(fields, field) + } + + sort.Strings(fields) + + if len(fields) > 0 { + buf.WriteByte(' ') + } + + // Move the "error" field to front + ei := sort.Search(len(fields), func(i int) bool { return fields[i] >= zerolog.ErrorFieldName }) + if ei < len(fields) && fields[ei] == zerolog.ErrorFieldName { + fields[ei] = "" + fields = append([]string{zerolog.ErrorFieldName}, fields...) + + var xfields = make([]string, 0, len(fields)) + + for _, field := range fields { + if field == "" { // Skip empty fields + continue + } + + xfields = append(xfields, field) + } + + fields = xfields + } + + for i, field := range fields { + var fn Formatter + + var fv Formatter + + if _, ok := w.formatters[field+"_field_name"]; ok { + fn = w.Formatter(field + "_field_name") + fv = w.Formatter(field + "_field_value") + } else { + fn = w.Formatter("field_name") + fv = w.Formatter("field_value") + } + + buf.WriteString(fn(field)) + buf.WriteString(fv(evt[field])) + + if i < len(fields)-1 { + buf.WriteByte(' ') + } + } +} + +func (w *ConsoleWriter) setDefaultFormatters() { + w.SetFormatter( + zerolog.TimestampFieldName, + func(i interface{}) string { + var t string + + if tt, ok := i.(string); ok { + ts, err := time.Parse(time.RFC3339, tt) + if err != nil { + t = tt + } else { + t = ts.Format(w.TimeFormat) + } + } + + return faint(t) + }) + w.SetFormatter( + zerolog.LevelFieldName, + + func(i interface{}) string { + var l string + + if ll, ok := i.(string); ok { + switch ll { + case "debug": + l = yellow("DBG") + case "info": + l = green("INF") + case "warn": + l = red("WRN") + case "error": + l = bold(red("ERR")) + case "fatal": + l = bold(red("FTL")) + case "panic": + l = bold(red("PNC")) + default: + l = bold("N/A") + } + } else { + l = strings.ToUpper(fmt.Sprintf("%s", i))[0:3] + } + + return l + }) + w.SetFormatter( + zerolog.CallerFieldName, + func(i interface{}) string { + var c string + if cc, ok := i.(string); ok { + c = cc + } + + if len(c) > 0 { + c = filepath.Base(filepath.Base(fmt.Sprintf("%s", i))) + } + + return faint(c) + }) + w.SetFormatter( + zerolog.MessageFieldName, + func(i interface{}) string { return fmt.Sprintf("%s", i) }) + w.SetFormatter( + "field_name", func(i interface{}) string { + return blue(fmt.Sprintf("%s=", i)) + }) + w.SetFormatter( + "field_value", func(i interface{}) string { + return fmt.Sprintf("%s", i) + }) + w.SetFormatter( + "error_field_name", func(i interface{}) string { + return faint(red(fmt.Sprintf("%s=", i))) + }) + w.SetFormatter( + "error_field_value", func(i interface{}) string { + return bold(red(fmt.Sprintf("%s", i))) + }) +} diff --git a/pkg/logx/consolelog/consolelog_test.go b/pkg/logx/consolelog/consolelog_test.go new file mode 100644 index 00000000..a709e3b8 --- /dev/null +++ b/pkg/logx/consolelog/consolelog_test.go @@ -0,0 +1,154 @@ +package consolelog_test + +import ( + "bytes" + "io" + "os" + "testing" + "time" + + "github.com/theopenlane/core/pkg/logx/consolelog" +) + +func TestConsolelog(t *testing.T) { + t.Run("Defaults", func(t *testing.T) { + w := consolelog.NewConsoleWriter() + + if w.TimeFormat == "" { + t.Errorf("Missing w.TimeFormat") + } + + if w.Formatter("foobar") == nil { + t.Errorf(`Missing default formatter for "foobar"`) + } + + d := time.Unix(0, 0).UTC().Format(time.RFC3339) + o := w.Formatter("time")(d) + if o != "12:00AM" { + t.Errorf(`Unexpected output for date %q: %s`, d, o) + } + }) + + t.Run("SetFormatter", func(t *testing.T) { + w := consolelog.NewConsoleWriter() + + w.SetFormatter("time", func(i interface{}) string { return "FOOBAR" }) + + d := time.Unix(0, 0).UTC().Format(time.RFC3339) + o := w.Formatter("time")(d) + if o != "FOOBAR" { + t.Errorf(`Unexpected output from custom "time" formatter: %s`, o) + } + }) + + t.Run("New with options", func(t *testing.T) { + w := consolelog.NewConsoleWriter( + func(w *consolelog.ConsoleWriter) { + w.SetFormatter("time", func(i interface{}) string { return "FOOBAR" }) + }, + ) + + d := time.Unix(0, 0).UTC().Format(time.RFC3339) + o := w.Formatter("time")(d) + if o != "FOOBAR" { + t.Errorf(`Unexpected output from custom "time" formatter: %s`, o) + } + }) + + t.Run("Write", func(t *testing.T) { + var out bytes.Buffer + w := consolelog.NewConsoleWriter() + w.Out = &out + + d := time.Unix(0, 0).UTC().Format(time.RFC3339) + _, err := w.Write([]byte(`{"time" : "` + d + `", "level" : "info", "message" : "Foobar"}`)) + if err != nil { + t.Errorf("Unexpected error when writing output: %s", err) + } + + expectedOutput := "12:00AM INF Foobar\n" + actualOutput := out.String() + if actualOutput != expectedOutput { + t.Errorf("Unexpected output %q, want: %q", actualOutput, expectedOutput) + } + }) + + t.Run("Write fields", func(t *testing.T) { + var out bytes.Buffer + w := consolelog.NewConsoleWriter() + w.Out = &out + + d := time.Unix(0, 0).UTC().Format(time.RFC3339) + _, err := w.Write([]byte(`{"time" : "` + d + `", "level" : "debug", "message" : "Foobar", "foo" : "bar"}`)) + if err != nil { + t.Errorf("Unexpected error when writing output: %s", err) + } + + expectedOutput := "12:00AM DBG Foobar foo=bar\n" + actualOutput := out.String() + if actualOutput != expectedOutput { + t.Errorf("Unexpected output %q, want: %q", actualOutput, expectedOutput) + } + }) + + t.Run("Write caller", func(t *testing.T) { + var out bytes.Buffer + w := consolelog.NewConsoleWriter() + w.Out = &out + + cwd, err := os.Getwd() + if err != nil { + t.Fatalf("Cannot get working directory: %s", err) + } + + d := time.Unix(0, 0).UTC().Format(time.RFC3339) + evt := `{"time" : "` + d + `", "level" : "debug", "message" : "Foobar", "foo" : "bar", "caller" : "` + cwd + `/foo/bar.go"}` + // t.Log(evt) + + _, err = w.Write([]byte(evt)) + if err != nil { + t.Errorf("Unexpected error when writing output: %s", err) + } + + expectedOutput := "12:00AM DBG bar.go Foobar foo=bar\n" + actualOutput := out.String() + if actualOutput != expectedOutput { + t.Errorf("Unexpected output %q, want: %q", actualOutput, expectedOutput) + } + }) + + t.Run("Write error", func(t *testing.T) { + var out bytes.Buffer + w := consolelog.NewConsoleWriter() + w.Out = &out + + d := time.Unix(0, 0).UTC().Format(time.RFC3339) + evt := `{"time" : "` + d + `", "level" : "error", "message" : "Foobar", "aaa" : "bbb", "error" : "Error"}` + // t.Log(evt) + + _, err := w.Write([]byte(evt)) + if err != nil { + t.Errorf("Unexpected error when writing output: %s", err) + } + + expectedOutput := "12:00AM ERR Foobar error=Error aaa=bbb\n" + actualOutput := out.String() + if actualOutput != expectedOutput { + t.Errorf("Unexpected output %q, want: %q", actualOutput, expectedOutput) + } + }) +} + +func BenchmarkConsolelog(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + + var msg = []byte(`{"level" : "info", "foo" : "bar"}`) + + w := consolelog.NewConsoleWriter() + w.Out = io.Discard + + for i := 0; i < b.N; i++ { + w.Write(msg) + } +} diff --git a/pkg/logx/consolelog/doc.go b/pkg/logx/consolelog/doc.go new file mode 100644 index 00000000..bc45de98 --- /dev/null +++ b/pkg/logx/consolelog/doc.go @@ -0,0 +1,2 @@ +// Package console log is a zerolog consolewriter output formatter that can be used generically with any zerolog instantiation so that it's not specific to a particular application +package consolelog diff --git a/pkg/logx/consolelog/example_test.go b/pkg/logx/consolelog/example_test.go new file mode 100644 index 00000000..34ab773b --- /dev/null +++ b/pkg/logx/consolelog/example_test.go @@ -0,0 +1,40 @@ +package consolelog_test + +import ( + "fmt" + "strings" + "time" + + "github.com/rs/zerolog" + "github.com/theopenlane/core/pkg/logx/consolelog" +) + +func ExampleNewConsoleWriter() { + output := consolelog.NewConsoleWriter() + logger := zerolog.New(output) + + logger.Info().Str("foo", "bar").Msg("hello world") + // Output: INF hello world foo=bar +} + +func ExampleNewConsoleWriter_custom() { + output := consolelog.NewConsoleWriter( + // Customize time formatting + // + func(w *consolelog.ConsoleWriter) { + w.TimeFormat = time.RFC822 + }, + // Customize "level" formatting + // + func(w *consolelog.ConsoleWriter) { + w.SetFormatter( + zerolog.LevelFieldName, + func(i interface{}) string { return strings.ToUpper(fmt.Sprintf("%-5s", i)) }) + }, + ) + + logger := zerolog.New(output).With().Timestamp().Logger() + + logger.Info().Str("foo", "bar").Msg("hello world") + // => 19 Jul 18 15:50 CEST INFO hello world foo=bar +} diff --git a/pkg/logx/pzlog/pzlog.go b/pkg/logx/pzlog/pzlog.go new file mode 100644 index 00000000..94e20d5a --- /dev/null +++ b/pkg/logx/pzlog/pzlog.go @@ -0,0 +1,211 @@ +package pzlog + +import ( + "bytes" + "errors" + "fmt" + "io" + "math" + "os" + "reflect" + "sort" + "strings" + "text/template" + + "github.com/goccy/go-json" + "github.com/gookit/color" + "github.com/pterm/pterm" + "github.com/rs/zerolog" +) + +// Field represents a key-value pair in the log event +type Field struct { + Key string + Val string +} + +// Event represents a log event with a timestamp, level, message, and fields +type Event struct { + Timestamp string + Level string + Message string + Fields []Field +} + +// Formatter is a function type that formats a value as a string +type Formatter func(interface{}) string + +// PtermWriter is a custom writer for zerolog that formats log events using pterm +type PtermWriter struct { + MaxWidth int + Out io.Writer + + LevelStyles map[zerolog.Level]*pterm.Style + + Tmpl *template.Template + + DefaultKeyStyle func(string, zerolog.Level) *pterm.Style + DefaultValFormatter func(string, zerolog.Level) Formatter + + KeyStyles map[string]*pterm.Style + ValFormatters map[string]Formatter + + KeyOrderFunc func(string, string) bool +} + +// ErrCannotParseTemplate is an error indicating that a template cannot be parsed +var ErrCannotParseTemplate = errors.New("cannot parse template") + +// NewPtermWriter creates a new PtermWriter with the provided options +func NewPtermWriter(options ...func(*PtermWriter)) *PtermWriter { + pt := PtermWriter{ + MaxWidth: pterm.GetTerminalWidth(), + Out: os.Stdout, + LevelStyles: map[zerolog.Level]*pterm.Style{ + zerolog.TraceLevel: pterm.NewStyle(pterm.Bold, pterm.FgCyan), + zerolog.DebugLevel: pterm.NewStyle(pterm.Bold, pterm.FgBlue), + zerolog.InfoLevel: pterm.NewStyle(pterm.Bold, pterm.FgGreen), + zerolog.WarnLevel: pterm.NewStyle(pterm.Bold, pterm.FgYellow), + zerolog.ErrorLevel: pterm.NewStyle(pterm.Bold, pterm.FgRed), + zerolog.FatalLevel: pterm.NewStyle(pterm.Bold, pterm.FgRed), + zerolog.PanicLevel: pterm.NewStyle(pterm.Bold, pterm.FgRed), + zerolog.NoLevel: pterm.NewStyle(pterm.Bold, pterm.FgWhite), + }, + KeyStyles: map[string]*pterm.Style{ + zerolog.MessageFieldName: pterm.NewStyle(pterm.Bold, pterm.FgWhite), + zerolog.TimestampFieldName: pterm.NewStyle(pterm.Bold, pterm.FgGray), + zerolog.CallerFieldName: pterm.NewStyle(pterm.Bold, pterm.FgGray), + zerolog.ErrorFieldName: pterm.NewStyle(pterm.Bold, pterm.FgRed), + zerolog.ErrorStackFieldName: pterm.NewStyle(pterm.Bold, pterm.FgRed), + }, + ValFormatters: map[string]Formatter{}, + KeyOrderFunc: func(k1, k2 string) bool { + score := func(s string) string { + s = color.ClearCode(s) + if s == zerolog.TimestampFieldName { + return string([]byte{0, 0}) + } + if s == zerolog.CallerFieldName { + return string([]byte{0, 1}) + } + if s == zerolog.ErrorFieldName { + return string([]byte{math.MaxUint8, 0}) + } + if s == zerolog.ErrorStackFieldName { + return string([]byte{math.MaxUint8, 1}) + } + return s + } + return score(k1) < score(k2) + }, + } + + tmpl := `{{ .Timestamp }} {{ .Level }}  {{ .Message }} +{{- range $i, $field := .Fields }} +{{ space (totalLength 1 $.Timestamp $.Level) }}{{if (last $i $.Fields )}}└{{else}}├{{ end }} {{ .Key }}: {{ .Val }} +{{- end }} +` + t, err := template.New("event"). + Funcs(template.FuncMap{ + "space": func(n int) string { + return strings.Repeat(" ", n) + }, + "totalLength": func(n int, s ...string) int { + return len(color.ClearCode(strings.Join(s, ""))) - n + }, + "last": func(x int, a interface{}) bool { + return x == reflect.ValueOf(a).Len()-1 + }, + }). + Parse(tmpl) + + if err != nil { + panic(fmt.Errorf("%w: %s", ErrCannotParseTemplate, err)) + } + + pt.Tmpl = t + + pt.DefaultKeyStyle = func(_ string, lvl zerolog.Level) *pterm.Style { + return pt.LevelStyles[lvl] + } + + pt.DefaultValFormatter = func(key string, lvl zerolog.Level) Formatter { + return func(v interface{}) string { + return pterm.Sprint(v) + } + } + + for _, option := range options { + option(&pt) + } + + return &pt +} + +func (pw *PtermWriter) Write(p []byte) (n int, err error) { + return pw.Out.Write(p) +} + +func (pw *PtermWriter) WriteLevel(lvl zerolog.Level, p []byte) (n int, err error) { + var evt map[string]interface{} + + d := json.NewDecoder(bytes.NewReader(p)) + + d.UseNumber() + + err = d.Decode(&evt) + if err != nil { + return n, fmt.Errorf("cannot decode event: %w", err) + } + + var event Event + if ts, ok := evt[zerolog.TimestampFieldName]; ok { + event.Timestamp = pw.KeyStyles[zerolog.TimestampFieldName].Sprint(ts) + } + + event.Level = pw.LevelStyles[lvl].Sprint(lvl) + + if msg, ok := evt[zerolog.MessageFieldName]; ok { + event.Message = pw.KeyStyles[zerolog.MessageFieldName].Sprint(msg) + } + + event.Fields = make([]Field, 0, len(evt)) + + for k, v := range evt { + if k == zerolog.TimestampFieldName || + k == zerolog.LevelFieldName || + k == zerolog.MessageFieldName { + continue + } + + var key string + + if style, ok := pw.KeyStyles[k]; ok { + key = style.Sprint(k) + } else { + key = pw.DefaultKeyStyle(k, lvl).Sprint(k) + } + + var val string + if fn, ok := pw.ValFormatters[k]; ok { + val = fn(v) + } else { + val = pw.DefaultValFormatter(k, lvl)(v) + } + + event.Fields = append(event.Fields, Field{Key: key, Val: val}) + } + + sort.Slice(event.Fields, func(i, j int) bool { + return pw.KeyOrderFunc(event.Fields[i].Key, event.Fields[j].Key) + }) + + var buf bytes.Buffer + + err = pw.Tmpl.Execute(&buf, event) + if err != nil { + return n, fmt.Errorf("cannot execute template: %w", err) + } + + return pw.Out.Write(buf.Bytes()) +}