From 7ef97ddbd48c889ddaffc3c9816c9158faec460e Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Sun, 7 May 2023 18:44:58 -0500 Subject: [PATCH 01/16] Convert sloghuman to key=value pair format This format is consistent with Go's new official `slog`, `zerolog` and many others. It is also far more readable than JSON, and a growing number of libaries and tools are able to parse it. --- go.mod | 24 +++- go.sum | 42 +++---- internal/entryhuman/entry.go | 162 ++++++++++----------------- internal/entryhuman/entry_test.go | 48 +++++++- internal/entryhuman/json.go | 42 ------- sloggers/sloghuman/sloghuman.go | 4 +- sloggers/sloghuman/sloghuman_test.go | 18 ++- sloggers/slogtest/t.go | 4 +- 8 files changed, 165 insertions(+), 179 deletions(-) delete mode 100644 internal/entryhuman/json.go diff --git a/go.mod b/go.mod index ac067d3..c1fd46d 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,31 @@ module cdr.dev/slog -go 1.13 +go 1.20 require ( cloud.google.com/go v0.26.0 - github.com/alecthomas/chroma v0.10.0 - github.com/fatih/color v1.13.0 + github.com/charmbracelet/lipgloss v0.7.1 github.com/google/go-cmp v0.5.3 + github.com/muesli/termenv v0.15.1 go.opencensus.io v0.24.0 - go.uber.org/goleak v1.2.1 // indirect + go.uber.org/goleak v1.2.1 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 ) + +require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect + github.com/golang/protobuf v1.4.3 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + golang.org/x/net v0.0.0-20201110031124-69a78807bb2b // indirect + golang.org/x/sys v0.6.0 // indirect + golang.org/x/text v0.3.3 // indirect + google.golang.org/grpc v1.33.2 // indirect + google.golang.org/protobuf v1.25.0 // indirect +) diff --git a/go.sum b/go.sum index 7de8934..4b64d35 100644 --- a/go.sum +++ b/go.sum @@ -1,22 +1,20 @@ cloud.google.com/go v0.26.0 h1:e0WKqKTd5BnrG8aKH3J3h+QvEIQtSUcf2n5UZ5ZgLtQ= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek= -github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E= +github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c= 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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= -github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= -github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -39,21 +37,26 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3 h1:x95R7cp+rSeeqAMI2knLtQ0DKlaBhv2NrtrOvafPHRo= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U= -github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs= +github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= @@ -83,11 +86,10 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -121,9 +123,7 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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= diff --git a/internal/entryhuman/entry.go b/internal/entryhuman/entry.go index 4e741c9..d1eee3e 100644 --- a/internal/entryhuman/entry.go +++ b/internal/entryhuman/entry.go @@ -4,17 +4,15 @@ package entryhuman import ( "bytes" - "encoding/json" "fmt" "io" "os" - "path/filepath" - "runtime/debug" "strconv" "strings" "time" - "github.com/fatih/color" + "github.com/charmbracelet/lipgloss" + "github.com/muesli/termenv" "go.opencensus.io/trace" "golang.org/x/crypto/ssh/terminal" "golang.org/x/xerrors" @@ -34,13 +32,20 @@ func StripTimestamp(ent string) (time.Time, string, error) { // TimeFormat is a simplified RFC3339 format. const TimeFormat = "2006-01-02 15:04:05.000" -func c(w io.Writer, attrs ...color.Attribute) *color.Color { - c := color.New(attrs...) - c.DisableColor() +var ( + renderer = lipgloss.NewRenderer(os.Stdout, termenv.WithUnsafe()) + + loggerNameStyle = renderer.NewStyle().Foreground(lipgloss.Color("#A47DFF")) + keyStyle = renderer.NewStyle().Foreground(lipgloss.Color("#606366")) + multiLineKeyStyle = renderer.NewStyle().Foreground(lipgloss.Color("#79b8ff")) +) + +func render(w io.Writer, st lipgloss.Style, s string) string { if shouldColor(w) { - c.EnableColor() + ss := st.Render(s) + return ss } - return c + return s } // Fmt returns a human readable format for ent. @@ -50,26 +55,25 @@ func c(w io.Writer, attrs ...color.Attribute) *color.Color { // We also do not indent the fields as go's test does that automatically // for extra lines in a log so if we did it here, the fields would be indented // twice in test logs. So the Stderr logger indents all the fields itself. -func Fmt(w io.Writer, ent slog.SinkEntry) string { - ents := c(w, color.Reset).Sprint("") +func Fmt(buf io.StringWriter, termW io.Writer, ent slog.SinkEntry, +) { ts := ent.Time.Format(TimeFormat) - ents += ts + " " + buf.WriteString(ts + " ") - level := "[" + ent.Level.String() + "]" - level = c(w, levelColor(ent.Level)).Sprint(level) - ents += fmt.Sprintf("%v\t", level) + level := ent.Level.String() + if len(level) > 4 { + level = level[:4] + } + level = "[" + level + "]" + buf.WriteString(render(termW, levelStyle(ent.Level), level)) + buf.WriteString("\t") if len(ent.LoggerNames) > 0 { loggerName := "(" + quoteKey(strings.Join(ent.LoggerNames, ".")) + ")" - loggerName = c(w, color.FgMagenta).Sprint(loggerName) - ents += fmt.Sprintf("%v\t", loggerName) + buf.WriteString(render(termW, loggerNameStyle, loggerName)) + buf.WriteString("\t") } - hpath, hfn := humanPathAndFunc(ent.File, ent.Func) - loc := fmt.Sprintf("<%v:%v>\t%v", hpath, ent.Line, hfn) - loc = c(w, color.FgCyan).Sprint(loc) - ents += fmt.Sprintf("%v\t", loc) - var multilineKey string var multilineVal string msg := strings.TrimSpace(ent.Message) @@ -77,9 +81,10 @@ func Fmt(w io.Writer, ent slog.SinkEntry) string { multilineKey = "msg" multilineVal = msg msg = "..." + msg = quote(msg) + buf.WriteString(msg) + } - msg = quote(msg) - ents += msg if ent.SpanContext != (trace.SpanContext{}) { ent.Fields = append(slog.M( @@ -111,48 +116,56 @@ func Fmt(w io.Writer, ent slog.SinkEntry) string { multilineVal = s } - if len(ent.Fields) > 0 { - // No error is guaranteed due to slog.Map handling errors itself. - fields, _ := json.MarshalIndent(ent.Fields, "", "") - fields = bytes.ReplaceAll(fields, []byte(",\n"), []byte(", ")) - fields = bytes.ReplaceAll(fields, []byte("\n"), []byte("")) - fields = formatJSON(w, fields) - ents += "\t" + string(fields) + for i, f := range ent.Fields { + if i < len(ent.Fields) { + buf.WriteString("\t") + } + buf.WriteString(render(termW, keyStyle, f.Name+"=")) + valueStr := fmt.Sprintf("%+v", f.Value) + buf.WriteString(quote(valueStr)) } if multilineVal != "" { if msg != "..." { - ents += " ..." + buf.WriteString(" ...") } // Proper indentation. lines := strings.Split(multilineVal, "\n") for i, line := range lines[1:] { if line != "" { - lines[i+1] = c(w, color.Reset).Sprint("") + strings.Repeat(" ", len(multilineKey)+4) + line + lines[i+1] = strings.Repeat(" ", len(multilineKey)+4) + line } } multilineVal = strings.Join(lines, "\n") - multilineKey = c(w, color.FgBlue).Sprintf(`"%v"`, multilineKey) - ents += fmt.Sprintf("\n%v: %v", multilineKey, multilineVal) + multilineKey = render(termW, multiLineKeyStyle, multilineKey) + buf.WriteString("\n") + buf.WriteString(multilineKey) + buf.WriteString("= ") + buf.WriteString(multilineVal) } - - return ents } -func levelColor(level slog.Level) color.Attribute { +var ( + levelDebugStyle = renderer.NewStyle().Foreground(lipgloss.Color("#ffffff")) + levelInfoStyle = renderer.NewStyle().Foreground(lipgloss.Color("#0091FF")) + levelWarnStyle = renderer.NewStyle().Foreground(lipgloss.Color("#FFCF0D")) + levelErrorStyle = renderer.NewStyle().Foreground(lipgloss.Color("#FF5A0D")) +) + +func levelStyle(level slog.Level) lipgloss.Style { switch level { case slog.LevelDebug: - return color.Reset + return levelDebugStyle case slog.LevelInfo: - return color.FgBlue + return levelInfoStyle case slog.LevelWarn: - return color.FgYellow - case slog.LevelError: - return color.FgRed + return levelWarnStyle + case slog.LevelError, slog.LevelFatal, slog.LevelCritical: + return levelErrorStyle default: - return color.FgHiRed + panic("unknown level") } } @@ -196,64 +209,3 @@ func quoteKey(key string) string { // Replace spaces in the map keys with underscores. return strings.ReplaceAll(key, " ", "_") } - -var mainPackagePath string -var mainModulePath string - -func init() { - // Unfortunately does not work for tests yet :( - // See https://github.com/golang/go/issues/33976 - bi, ok := debug.ReadBuildInfo() - if !ok { - return - } - mainPackagePath = bi.Path - mainModulePath = bi.Main.Path -} - -// humanPathAndFunc takes the absolute path to a file and an absolute module path to a -// function in that file and returns the module path to the file. It also returns -// the path to the function stripped of its module prefix. -// -// If the file is in the main Go module then its path is returned -// relative to the main Go module's root. -// -// fn is from https://pkg.go.dev/runtime#Func.Name -func humanPathAndFunc(filename, fn string) (hpath, hfn string) { - // pkgDir is the dir of the pkg. - // e.g. cdr.dev/slog/internal - // base is the package name and the function name separated by a period. - // e.g. entryhuman.humanPathAndFunc - // There can be multiple periods when methods of types are involved. - pkgDir, base := filepath.Split(fn) - s := strings.Split(base, ".") - pkg := s[0] - hfn = strings.Join(s[1:], ".") - - if pkg == "main" { - // This happens with go build main.go - if mainPackagePath == "command-line-arguments" { - // Without a real mainPath, we can't find the path to the file - // relative to the module. So we just return the base. - return filepath.Base(filename), hfn - } - // Go doesn't return the full main path in runtime.Func.Name() - // It just returns the path "main" - // Only runtime.ReadBuildInfo returns it so we have to check and replace. - pkgDir = mainPackagePath - // pkg main isn't reflected on the disk so we should not add it - // into the pkgpath. - pkg = "" - } - - hpath = filepath.Join(pkgDir, pkg, filepath.Base(filename)) - - if mainModulePath != "" { - relhpath, err := filepath.Rel(mainModulePath, hpath) - if err == nil { - hpath = "./" + relhpath - } - } - - return hpath, hfn -} diff --git a/internal/entryhuman/entry_test.go b/internal/entryhuman/entry_test.go index f7fc596..41018f4 100644 --- a/internal/entryhuman/entry_test.go +++ b/internal/entryhuman/entry_test.go @@ -1,7 +1,10 @@ package entryhuman_test import ( + "fmt" + "io" "io/ioutil" + "strings" "testing" "time" @@ -18,8 +21,9 @@ func TestEntry(t *testing.T) { t.Parallel() test := func(t *testing.T, in slog.SinkEntry, exp string) { - act := entryhuman.Fmt(ioutil.Discard, in) - assert.Equal(t, "entry", exp, act) + var sb strings.Builder + entryhuman.Fmt(&sb, ioutil.Discard, in) + assert.Equal(t, "entry", exp, sb.String()) } t.Run("basic", func(t *testing.T) { @@ -83,12 +87,48 @@ func TestEntry(t *testing.T) { t.Run("color", func(t *testing.T) { t.Parallel() - act := entryhuman.Fmt(entryhuman.ForceColorWriter, slog.SinkEntry{ + var sb strings.Builder + entryhuman.Fmt(&sb, entryhuman.ForceColorWriter, slog.SinkEntry{ Level: slog.LevelCritical, Fields: slog.M( slog.F("hey", "hi"), ), }) - assert.Equal(t, "entry", "\x1b[0m\x1b[0m0001-01-01 00:00:00.000 \x1b[91m[CRITICAL]\x1b[0m\t\x1b[36m<.:0> \x1b[0m\t\"\"\t{\x1b[34m\"hey\"\x1b[0m: \x1b[32m\"hi\"\x1b[0m}", act) + assert.Equal(t, "entry", "\x1b[0m\x1b[0m0001-01-01 00:00:00.000 \x1b[91m[CRITICAL]\x1b[0m\t\x1b[36m<.:0> \x1b[0m\t\"\"\t{\x1b[34m\"hey\"\x1b[0m: \x1b[32m\"hi\"\x1b[0m}", sb.String()) }) } + +func BenchmarkFmt(b *testing.B) { + bench := func(b *testing.B, color bool) { + nfs := []int{1, 4, 16} + for _, nf := range nfs { + name := fmt.Sprintf("nf=%v", nf) + if color { + name = "Colored-" + name + } + b.Run(name, func(b *testing.B) { + fs := make([]slog.Field, nf) + for i := 0; i < nf; i++ { + fs[i] = slog.F("key", "value") + } + se := slog.SinkEntry{ + Level: slog.LevelCritical, + Fields: slog.M( + fs..., + ), + } + w := io.Discard + if color { + w = entryhuman.ForceColorWriter + } + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + entryhuman.Fmt(io.Discard.(io.StringWriter), w, se) + } + }) + } + } + bench(b, true) + bench(b, false) +} diff --git a/internal/entryhuman/json.go b/internal/entryhuman/json.go deleted file mode 100644 index 25f65f6..0000000 --- a/internal/entryhuman/json.go +++ /dev/null @@ -1,42 +0,0 @@ -package entryhuman - -import ( - "bytes" - "io" - - "github.com/alecthomas/chroma" - "github.com/alecthomas/chroma/formatters" - jlexers "github.com/alecthomas/chroma/lexers/j" -) - -// Adapted from https://github.com/alecthomas/chroma/blob/2f5349aa18927368dbec6f8c11608bf61c38b2dd/styles/bw.go#L7 -// https://github.com/alecthomas/chroma/blob/2f5349aa18927368dbec6f8c11608bf61c38b2dd/formatters/tty_indexed.go -// https://github.com/alecthomas/chroma/blob/2f5349aa18927368dbec6f8c11608bf61c38b2dd/lexers/j/json.go -var style = chroma.MustNewStyle("slog", chroma.StyleEntries{ - // Magenta. - chroma.Keyword: "#7f007f", - // Magenta. - chroma.Number: "#7f007f", - // Magenta. - chroma.Name: "#00007f", - // Green. - chroma.String: "#007f00", -}) - -var jsonLexer = chroma.Coalesce(jlexers.JSON) - -func formatJSON(w io.Writer, buf []byte) []byte { - if !shouldColor(w) { - return buf - } - - highlighted, _ := colorizeJSON(buf) - return highlighted -} - -func colorizeJSON(buf []byte) ([]byte, error) { - it, _ := jsonLexer.Tokenise(nil, string(buf)) - b := &bytes.Buffer{} - formatters.TTY8.Format(b, style, it) - return b.Bytes(), nil -} diff --git a/sloggers/sloghuman/sloghuman.go b/sloggers/sloghuman/sloghuman.go index b872c74..ebd6a01 100644 --- a/sloggers/sloghuman/sloghuman.go +++ b/sloggers/sloghuman/sloghuman.go @@ -30,7 +30,9 @@ type humanSink struct { } func (s humanSink) LogEntry(ctx context.Context, ent slog.SinkEntry) { - str := entryhuman.Fmt(s.w2, ent) + var sb strings.Builder + entryhuman.Fmt(&sb, s.w2, ent) + str := sb.String() lines := strings.Split(str, "\n") // We need to add 4 spaces before every field line for readability. diff --git a/sloggers/sloghuman/sloghuman_test.go b/sloggers/sloghuman/sloghuman_test.go index 1c26631..483a3b6 100644 --- a/sloggers/sloghuman/sloghuman_test.go +++ b/sloggers/sloghuman/sloghuman_test.go @@ -3,6 +3,7 @@ package sloghuman_test import ( "bytes" "context" + "os" "testing" "cdr.dev/slog" @@ -24,5 +25,20 @@ func TestMake(t *testing.T) { et, rest, err := entryhuman.StripTimestamp(b.String()) assert.Success(t, "strip timestamp", err) assert.False(t, "timestamp", et.IsZero()) - assert.Equal(t, "entry", " [INFO]\t\tTestMake\t...\t{\"wowow\": \"me\\nyou\"}\n \"msg\": line1\n\n line2\n", rest) + assert.Equal(t, "entry", " [INFO]\t...\twowow=\"me\\nyou\"\n \"msg\"= line1\n\n line2\n", rest) +} + +func TestVisual(t *testing.T) { + t.Setenv("FORCE_COLOR", "true") + if os.Getenv("TEST_VISUAL") == "" { + t.Skip("TEST_VISUAL not set") + } + + l := slog.Make(sloghuman.Sink(os.Stdout)).Leveled(slog.LevelDebug) + l.Debug(bg, "small potatos", slog.F("aaa", "mmm"), slog.F("bbb", "nnn"), slog.F("age", 24)) + l.Info(bg, "line1\n\nline2", slog.F("wowow", "me\nyou")) + l.Warn(bg, "oops", slog.F("aaa", "mmm")) + l = l.Named("sublogger") + l.Error(bg, "big oops", slog.F("aaa", "mmm")) + l.Sync() } diff --git a/sloggers/slogtest/t.go b/sloggers/slogtest/t.go index 646a03d..ace01c8 100644 --- a/sloggers/slogtest/t.go +++ b/sloggers/slogtest/t.go @@ -9,6 +9,7 @@ import ( "context" "log" "os" + "strings" "sync" "testing" @@ -73,8 +74,9 @@ func (ts *testSink) LogEntry(ctx context.Context, ent slog.SinkEntry) { return } + var s strings.Builder // The testing package logs to stdout and not stderr. - s := entryhuman.Fmt(os.Stdout, ent) + entryhuman.Fmt(&s, os.Stdout, ent) switch ent.Level { case slog.LevelDebug, slog.LevelInfo, slog.LevelWarn: From 5435c2740ccd9e4696b7a546eac9364d47e1ea2b Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Mon, 8 May 2023 11:33:18 -0500 Subject: [PATCH 02/16] Tests almost pass --- internal/entryhuman/entry.go | 2 +- internal/entryhuman/entry_test.go | 154 ++++++++++-------- internal/entryhuman/testdata/funky.golden | 1 + .../entryhuman/testdata/multilineField.golden | 3 + .../testdata/multilineMessage.golden | 3 + internal/entryhuman/testdata/named.golden | 1 + .../entryhuman/testdata/simpleNoFields.golden | 1 + sloggers/sloghuman/sloghuman_test.go | 2 +- 8 files changed, 94 insertions(+), 73 deletions(-) create mode 100644 internal/entryhuman/testdata/funky.golden create mode 100644 internal/entryhuman/testdata/multilineField.golden create mode 100644 internal/entryhuman/testdata/multilineMessage.golden create mode 100644 internal/entryhuman/testdata/named.golden create mode 100644 internal/entryhuman/testdata/simpleNoFields.golden diff --git a/internal/entryhuman/entry.go b/internal/entryhuman/entry.go index d1eee3e..3a803ac 100644 --- a/internal/entryhuman/entry.go +++ b/internal/entryhuman/entry.go @@ -134,7 +134,7 @@ func Fmt(buf io.StringWriter, termW io.Writer, ent slog.SinkEntry, lines := strings.Split(multilineVal, "\n") for i, line := range lines[1:] { if line != "" { - lines[i+1] = strings.Repeat(" ", len(multilineKey)+4) + line + lines[i+1] = strings.Repeat(" ", len(multilineKey)+2) + line } } multilineVal = strings.Join(lines, "\n") diff --git a/internal/entryhuman/entry_test.go b/internal/entryhuman/entry_test.go index 41018f4..f8869c2 100644 --- a/internal/entryhuman/entry_test.go +++ b/internal/entryhuman/entry_test.go @@ -1,15 +1,15 @@ package entryhuman_test import ( + "bytes" + "flag" "fmt" "io" "io/ioutil" - "strings" + "os" "testing" "time" - "go.opencensus.io/trace" - "cdr.dev/slog" "cdr.dev/slog/internal/assert" "cdr.dev/slog/internal/entryhuman" @@ -17,85 +17,97 @@ import ( var kt = time.Date(2000, time.February, 5, 4, 4, 4, 4, time.UTC) +var updateGoldenFiles = flag.Bool("update-golden-files", false, "update golden files in testdata") + func TestEntry(t *testing.T) { t.Parallel() - test := func(t *testing.T, in slog.SinkEntry, exp string) { - var sb strings.Builder - entryhuman.Fmt(&sb, ioutil.Discard, in) - assert.Equal(t, "entry", exp, sb.String()) + type tcase struct { + name string + ent slog.SinkEntry } - t.Run("basic", func(t *testing.T) { - t.Parallel() - - test(t, slog.SinkEntry{ - Message: "wowowow\tizi", - Time: kt, - Level: slog.LevelDebug, - - File: "myfile", - Line: 100, - Func: "mypkg.ignored", - }, `2000-02-05 04:04:04.000 [DEBUG] ignored "wowowow\tizi"`) - }) - - t.Run("multilineMessage", func(t *testing.T) { - t.Parallel() - - test(t, slog.SinkEntry{ - Message: "line1\nline2", - Level: slog.LevelInfo, - }, `0001-01-01 00:00:00.000 [INFO] <.:0> ... -"msg": line1 - line2`) - }) - - t.Run("multilineField", func(t *testing.T) { - t.Parallel() - - test(t, slog.SinkEntry{ - Message: "msg", - Level: slog.LevelInfo, - Fields: slog.M(slog.F("field", "line1\nline2")), - }, `0001-01-01 00:00:00.000 [INFO] <.:0> msg ... -"field": line1 - line2`) - }) - - t.Run("named", func(t *testing.T) { - t.Parallel() + ents := []tcase{ + { + "simpleNoFields", + slog.SinkEntry{ + Message: "wowowow\tizi", + Time: kt, + Level: slog.LevelDebug, + + File: "myfile", + Line: 100, + Func: "mypkg.ignored", + }, + }, + { + "multilineMessage", + slog.SinkEntry{ + Message: "line1\nline2", + Level: slog.LevelInfo, + }, + }, + { + "multilineField", + slog.SinkEntry{ + Message: "msg", + Level: slog.LevelInfo, + Fields: slog.M(slog.F("field", "line1\nline2")), + }, + }, + { + "named", + slog.SinkEntry{ + Level: slog.LevelWarn, + LoggerNames: []string{"named", "meow"}, + }, + }, + { + "funky", + slog.SinkEntry{ + Level: slog.LevelWarn, + Fields: slog.M( + slog.F("funky^%&^&^key", "value"), + slog.F("funky^%&^&^key2", "@#\t \t \n"), + ), + }, + }, + } + if *updateGoldenFiles { + ents, err := os.ReadDir("testdata") + if err != nil { + t.Fatal(err) + } + for _, ent := range ents { + os.Remove("testdata/" + ent.Name()) + } + } - test(t, slog.SinkEntry{ - Level: slog.LevelWarn, - LoggerNames: []string{"named", "meow"}, - }, `0001-01-01 00:00:00.000 [WARN] (named.meow) <.:0> ""`) - }) + for _, tc := range ents { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + goldenPath := fmt.Sprintf("testdata/%s.golden", tc.name) - t.Run("trace", func(t *testing.T) { - t.Parallel() + var gotBuf bytes.Buffer + entryhuman.Fmt(&gotBuf, ioutil.Discard, tc.ent) - test(t, slog.SinkEntry{ - Level: slog.LevelError, - SpanContext: trace.SpanContext{ - SpanID: trace.SpanID{0, 1, 2, 3, 4, 5, 6, 7}, - TraceID: trace.TraceID{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, - }, - }, `0001-01-01 00:00:00.000 [ERROR] <.:0> "" {"trace": "000102030405060708090a0b0c0d0e0f", "span": "0001020304050607"}`) - }) + if *updateGoldenFiles { + err := os.WriteFile(goldenPath, gotBuf.Bytes(), 0o644) + if err != nil { + t.Fatal(err) + } + return + } - t.Run("color", func(t *testing.T) { - t.Parallel() + wantByt, err := os.ReadFile(goldenPath) + if err != nil { + t.Fatal(err) + } - var sb strings.Builder - entryhuman.Fmt(&sb, entryhuman.ForceColorWriter, slog.SinkEntry{ - Level: slog.LevelCritical, - Fields: slog.M( - slog.F("hey", "hi"), - ), + assert.Equal(t, "entry matches", string(wantByt), gotBuf.String()) }) - assert.Equal(t, "entry", "\x1b[0m\x1b[0m0001-01-01 00:00:00.000 \x1b[91m[CRITICAL]\x1b[0m\t\x1b[36m<.:0> \x1b[0m\t\"\"\t{\x1b[34m\"hey\"\x1b[0m: \x1b[32m\"hi\"\x1b[0m}", sb.String()) - }) + } } func BenchmarkFmt(b *testing.B) { diff --git a/internal/entryhuman/testdata/funky.golden b/internal/entryhuman/testdata/funky.golden new file mode 100644 index 0000000..f11a873 --- /dev/null +++ b/internal/entryhuman/testdata/funky.golden @@ -0,0 +1 @@ +0001-01-01 00:00:00.000 [WARN] funky^%&^&^key=value funky^%&^&^key2="@#\t \t \n" \ No newline at end of file diff --git a/internal/entryhuman/testdata/multilineField.golden b/internal/entryhuman/testdata/multilineField.golden new file mode 100644 index 0000000..a24113f --- /dev/null +++ b/internal/entryhuman/testdata/multilineField.golden @@ -0,0 +1,3 @@ +0001-01-01 00:00:00.000 [INFO] ... +field= line1 + line2 \ No newline at end of file diff --git a/internal/entryhuman/testdata/multilineMessage.golden b/internal/entryhuman/testdata/multilineMessage.golden new file mode 100644 index 0000000..17e479f --- /dev/null +++ b/internal/entryhuman/testdata/multilineMessage.golden @@ -0,0 +1,3 @@ +0001-01-01 00:00:00.000 [INFO] ... +msg= line1 + line2 \ No newline at end of file diff --git a/internal/entryhuman/testdata/named.golden b/internal/entryhuman/testdata/named.golden new file mode 100644 index 0000000..e896ca2 --- /dev/null +++ b/internal/entryhuman/testdata/named.golden @@ -0,0 +1 @@ +0001-01-01 00:00:00.000 [WARN] (named.meow) \ No newline at end of file diff --git a/internal/entryhuman/testdata/simpleNoFields.golden b/internal/entryhuman/testdata/simpleNoFields.golden new file mode 100644 index 0000000..9143ac6 --- /dev/null +++ b/internal/entryhuman/testdata/simpleNoFields.golden @@ -0,0 +1 @@ +2000-02-05 04:04:04.000 [DEBU] \ No newline at end of file diff --git a/sloggers/sloghuman/sloghuman_test.go b/sloggers/sloghuman/sloghuman_test.go index 483a3b6..cd01c89 100644 --- a/sloggers/sloghuman/sloghuman_test.go +++ b/sloggers/sloghuman/sloghuman_test.go @@ -25,7 +25,7 @@ func TestMake(t *testing.T) { et, rest, err := entryhuman.StripTimestamp(b.String()) assert.Success(t, "strip timestamp", err) assert.False(t, "timestamp", et.IsZero()) - assert.Equal(t, "entry", " [INFO]\t...\twowow=\"me\\nyou\"\n \"msg\"= line1\n\n line2\n", rest) + assert.Equal(t, "entry", " [INFO]\t...\twowow=\"me\\nyou\"\n msg= line1\n\n line2\n", rest) } func TestVisual(t *testing.T) { From e2270d803ab4f936c0e2914b117ae2214e1618ec Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Mon, 8 May 2023 12:01:08 -0500 Subject: [PATCH 03/16] Quote more aggressively --- example_test.go | 27 +++++++--------------- internal/entryhuman/entry.go | 14 ++++++++--- internal/entryhuman/entry_test.go | 9 ++++++++ internal/entryhuman/testdata/spacey.golden | 1 + 4 files changed, 29 insertions(+), 22 deletions(-) create mode 100644 internal/entryhuman/testdata/spacey.golden diff --git a/example_test.go b/example_test.go index 2853ff7..95131ee 100644 --- a/example_test.go +++ b/example_test.go @@ -8,7 +8,6 @@ import ( "testing" "time" - "go.opencensus.io/trace" "golang.org/x/xerrors" "cdr.dev/slog" @@ -72,23 +71,13 @@ func Example_testing() { slog.F("field_name", "something or the other"), ) - // t.go:55: 2019-12-05 21:20:31.218 [INFO] my message here {"field_name": "something or the other"} -} - -func Example_tracing() { - log := slog.Make(sloghuman.Sink(os.Stdout)) - - ctx, _ := trace.StartSpan(context.Background(), "spanName") - - log.Info(ctx, "my msg", slog.F("hello", "hi")) - - // 2019-12-09 21:59:48.110 [INFO] my msg {"trace": "f143d018d00de835688453d8dc55c9fd", "span": "f214167bf550afc3", "hello": "hi"} + // t.go:55: 2019-12-05 21:20:31.218 [INFO] my message here field_name="something or the other" } func Example_multiple() { l := slog.Make(sloghuman.Sink(os.Stdout)) - f, err := os.OpenFile("stackdriver", os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644) + f, err := os.OpenFile("stackdriver", os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o644) if err != nil { l.Fatal(context.Background(), "failed to open stackdriver log file", slog.Error(err)) } @@ -97,7 +86,7 @@ func Example_multiple() { l.Info(context.Background(), "log to stdout and stackdriver") - // 2019-12-07 20:59:55.790 [INFO] log to stdout and stackdriver + // 2019-12-07 20:59:55.790 [INFO] log to stdout and stackdriver } func ExampleWith() { @@ -106,7 +95,7 @@ func ExampleWith() { l := slog.Make(sloghuman.Sink(os.Stdout)) l.Info(ctx, "msg") - // 2019-12-07 20:54:23.986 [INFO] msg {"field": 1} + // 2019-12-07 20:54:23.986 [INFO] msg field=1} } func ExampleStdlib() { @@ -115,7 +104,7 @@ func ExampleStdlib() { l.Print("msg") - // 2019-12-07 20:54:23.986 [INFO] (stdlib) msg {"field": 1} + // 2019-12-07 20:54:23.986 [INFO] (stdlib) msg field=1 } func ExampleLogger_Named() { @@ -125,7 +114,7 @@ func ExampleLogger_Named() { l = l.Named("http") l.Info(ctx, "received request", slog.F("remote address", net.IPv4(127, 0, 0, 1))) - // 2019-12-07 21:20:56.974 [INFO] (http) received request {"remote address": "127.0.0.1"} + // 2019-12-07 21:20:56.974 [INFO] (http) received request remote_address=127.0.0.1} } func ExampleLogger_Leveled() { @@ -139,6 +128,6 @@ func ExampleLogger_Leveled() { l.Debug(ctx, "testing2") - // 2019-12-07 21:26:20.945 [INFO] received request - // 2019-12-07 21:26:20.945 [DEBUG] testing2 + // 2019-12-07 21:26:20.945 [INFO] received request + // 2019-12-07 21:26:20.945 [DEBU] testing2 } diff --git a/internal/entryhuman/entry.go b/internal/entryhuman/entry.go index 3a803ac..5f15d75 100644 --- a/internal/entryhuman/entry.go +++ b/internal/entryhuman/entry.go @@ -10,6 +10,7 @@ import ( "strconv" "strings" "time" + "unicode" "github.com/charmbracelet/lipgloss" "github.com/muesli/termenv" @@ -120,7 +121,7 @@ func Fmt(buf io.StringWriter, termW io.Writer, ent slog.SinkEntry, if i < len(ent.Fields) { buf.WriteString("\t") } - buf.WriteString(render(termW, keyStyle, f.Name+"=")) + buf.WriteString(render(termW, keyStyle, quoteKey(f.Name)+"=")) valueStr := fmt.Sprintf("%+v", f.Value) buf.WriteString(quote(valueStr)) } @@ -195,11 +196,18 @@ func quote(key string) string { return `""` } + var hasSpace bool + for _, r := range key { + if unicode.IsSpace(r) { + hasSpace = true + break + } + } quoted := strconv.Quote(key) // If the key doesn't need to be quoted, don't quote it. // We do not use strconv.CanBackquote because it doesn't // account tabs. - if quoted[1:len(quoted)-1] == key { + if !hasSpace && quoted[1:len(quoted)-1] == key { return key } return quoted @@ -207,5 +215,5 @@ func quote(key string) string { func quoteKey(key string) string { // Replace spaces in the map keys with underscores. - return strings.ReplaceAll(key, " ", "_") + return quote(strings.ReplaceAll(key, " ", "_")) } diff --git a/internal/entryhuman/entry_test.go b/internal/entryhuman/entry_test.go index f8869c2..157617e 100644 --- a/internal/entryhuman/entry_test.go +++ b/internal/entryhuman/entry_test.go @@ -72,6 +72,15 @@ func TestEntry(t *testing.T) { ), }, }, + { + "spacey", + slog.SinkEntry{ + Level: slog.LevelWarn, + Fields: slog.M( + slog.F("space in my key", "value in my value"), + ), + }, + }, } if *updateGoldenFiles { ents, err := os.ReadDir("testdata") diff --git a/internal/entryhuman/testdata/spacey.golden b/internal/entryhuman/testdata/spacey.golden new file mode 100644 index 0000000..4a01d24 --- /dev/null +++ b/internal/entryhuman/testdata/spacey.golden @@ -0,0 +1 @@ +0001-01-01 00:00:00.000 [WARN] space_in_my_key="value in my value" \ No newline at end of file From cb746033dd15b43d4cb8c503ddec7bb5d81fbdbc Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Mon, 8 May 2023 14:35:36 -0500 Subject: [PATCH 04/16] Re-add message --- internal/entryhuman/entry.go | 16 +++++++++++++--- internal/entryhuman/entry_test.go | 2 +- .../entryhuman/testdata/multilineField.golden | 2 +- .../entryhuman/testdata/simpleNoFields.golden | 2 +- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/internal/entryhuman/entry.go b/internal/entryhuman/entry.go index 5f15d75..7d31894 100644 --- a/internal/entryhuman/entry.go +++ b/internal/entryhuman/entry.go @@ -49,6 +49,12 @@ func render(w io.Writer, st lipgloss.Style, s string) string { return s } +func reset(w io.Writer, termW io.Writer) { + if shouldColor(termW) { + fmt.Fprintf(w, termenv.CSI+termenv.ResetSeq+"m") + } +} + // Fmt returns a human readable format for ent. // // We never return with a trailing newline because Go's testing framework adds one @@ -56,8 +62,13 @@ func render(w io.Writer, st lipgloss.Style, s string) string { // We also do not indent the fields as go's test does that automatically // for extra lines in a log so if we did it here, the fields would be indented // twice in test logs. So the Stderr logger indents all the fields itself. -func Fmt(buf io.StringWriter, termW io.Writer, ent slog.SinkEntry, +func Fmt( + buf interface { + io.StringWriter + io.Writer + }, termW io.Writer, ent slog.SinkEntry, ) { + reset(buf, termW) ts := ent.Time.Format(TimeFormat) buf.WriteString(ts + " ") @@ -83,9 +94,8 @@ func Fmt(buf io.StringWriter, termW io.Writer, ent slog.SinkEntry, multilineVal = msg msg = "..." msg = quote(msg) - buf.WriteString(msg) - } + buf.WriteString(msg) if ent.SpanContext != (trace.SpanContext{}) { ent.Fields = append(slog.M( diff --git a/internal/entryhuman/entry_test.go b/internal/entryhuman/entry_test.go index 157617e..1da603d 100644 --- a/internal/entryhuman/entry_test.go +++ b/internal/entryhuman/entry_test.go @@ -145,7 +145,7 @@ func BenchmarkFmt(b *testing.B) { b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { - entryhuman.Fmt(io.Discard.(io.StringWriter), w, se) + entryhuman.Fmt(bytes.NewBuffer(nil), w, se) } }) } diff --git a/internal/entryhuman/testdata/multilineField.golden b/internal/entryhuman/testdata/multilineField.golden index a24113f..1a94b70 100644 --- a/internal/entryhuman/testdata/multilineField.golden +++ b/internal/entryhuman/testdata/multilineField.golden @@ -1,3 +1,3 @@ -0001-01-01 00:00:00.000 [INFO] ... +0001-01-01 00:00:00.000 [INFO] msg ... field= line1 line2 \ No newline at end of file diff --git a/internal/entryhuman/testdata/simpleNoFields.golden b/internal/entryhuman/testdata/simpleNoFields.golden index 9143ac6..b36ae73 100644 --- a/internal/entryhuman/testdata/simpleNoFields.golden +++ b/internal/entryhuman/testdata/simpleNoFields.golden @@ -1 +1 @@ -2000-02-05 04:04:04.000 [DEBU] \ No newline at end of file +2000-02-05 04:04:04.000 [DEBU] wowowow izi \ No newline at end of file From c2245fa86cfa58b70a62a1d1fe15793fb20992db Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Mon, 8 May 2023 14:41:30 -0500 Subject: [PATCH 05/16] Tests pass! --- s_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/s_test.go b/s_test.go index 4e3b9e1..3914a7d 100644 --- a/s_test.go +++ b/s_test.go @@ -23,5 +23,5 @@ func TestStdlib(t *testing.T) { et, rest, err := entryhuman.StripTimestamp(b.String()) assert.Success(t, "strip timestamp", err) assert.False(t, "timestamp", et.IsZero()) - assert.Equal(t, "entry", " [INFO]\t(stdlib)\t\tTestStdlib\tstdlib\t{\"hi\": \"we\"}\n", rest) + assert.Equal(t, "entry", " [INFO]\t(stdlib)\tstdlib\thi=we\n", rest) } From 1f72346207c2366ed251080490ff0daf16734c74 Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Mon, 8 May 2023 14:55:36 -0500 Subject: [PATCH 06/16] bump actions checkout --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3d75a3..ce3ab2c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: fmt: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: make fmt uses: ./ci/image @@ -25,7 +25,7 @@ jobs: lint: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: make lint uses: ./ci/image @@ -35,7 +35,7 @@ jobs: test: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: make test uses: ./ci/image From 0870e1ac7caa4d014f8b433010e27b1f08c86f2d Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Mon, 8 May 2023 15:08:30 -0500 Subject: [PATCH 07/16] consolidate CI --- .github/workflows/ci.yml | 39 +++++++++---------------- ci/fmt.mk | 9 ++---- ci/image/Dockerfile | 14 --------- ci/lint.mk | 2 +- ci/test.mk | 2 +- internal/entryhuman/entry.go | 4 +-- map_test.go | 6 +--- sloggers/slogtest/assert/assert.go | 1 - sloggers/slogtest/assert/assert_test.go | 3 +- 9 files changed, 22 insertions(+), 58 deletions(-) delete mode 100644 ci/image/Dockerfile diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ce3ab2c..4a212ec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,38 +12,25 @@ on: workflow_dispatch: jobs: - fmt: - runs-on: ubuntu-20.04 + go: + runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - - name: make fmt - uses: ./ci/image + - name: Cache npm + uses: actions/cache@v3 with: - args: make fmt - - lint: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v3 - - - name: make lint - uses: ./ci/image + path: ~/.npm + key: "npm-cache" + - uses: actions/setup-go@v4 with: - args: make lint - - test: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v3 - - - name: make test - uses: ./ci/image - with: - args: make test + go-version: "1.20" + cache-dependency-path: go.sum + - name: "make" + run: | + git config --global --add safe.directory /github/workspace + make -O -j fmt lint test env: COVERALLS_TOKEN: ${{ secrets.github_token }} - - name: Upload coverage.html uses: actions/upload-artifact@v2 with: diff --git a/ci/fmt.mk b/ci/fmt.mk index 026cc36..7f74874 100644 --- a/ci/fmt.mk +++ b/ci/fmt.mk @@ -1,4 +1,4 @@ -fmt: modtidy gofmt goimports prettier +fmt: modtidy gofmt prettier ifdef CI if [[ $$(git ls-files --other --modified --exclude-standard) != "" ]]; then echo "Files need generation or are formatted incorrectly:" @@ -13,13 +13,10 @@ modtidy: gen go mod tidy gofmt: gen - gofmt -w -s . - -goimports: gen - goimports -w "-local=$$(go list -m)" . + go run mvdan.cc/gofumpt@latest -w . prettier: - prettier --write --print-width=120 --no-semi --trailing-comma=all --loglevel=warn $$(git ls-files "*.yml") + npx prettier --write --print-width=120 --no-semi --trailing-comma=all --loglevel=warn $$(git ls-files "*.yml") gen: go generate ./... diff --git a/ci/image/Dockerfile b/ci/image/Dockerfile deleted file mode 100644 index 2c6988e..0000000 --- a/ci/image/Dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -FROM golang:1 - -RUN apt-get update && \ - apt-get install -y npm - -ENV GOFLAGS="-mod=readonly" -ENV PAGER=cat -ENV CI=true -ENV MAKEFLAGS="--jobs=8 --output-sync=target" - -RUN npm install -g prettier -RUN go install golang.org/x/tools/cmd/goimports@latest -RUN go install golang.org/x/lint/golint@latest -RUN go install github.com/mattn/goveralls@latest diff --git a/ci/lint.mk b/ci/lint.mk index fbf42d2..36da85b 100644 --- a/ci/lint.mk +++ b/ci/lint.mk @@ -4,4 +4,4 @@ govet: go vet ./... golint: - golint -set_exit_status ./... + go run github.com/golangci/golangci-lint/cmd/golangci-lint@latest run . diff --git a/ci/test.mk b/ci/test.mk index 2615c51..35bfd05 100644 --- a/ci/test.mk +++ b/ci/test.mk @@ -15,7 +15,7 @@ coveralls: gotest export CI_PULL_REQUEST="$$(jq .number "$$GITHUB_EVENT_PATH")" BUILD_NUMBER="$$BUILD_NUMBER-PR-$$CI_PULL_REQUEST" fi - goveralls -coverprofile=ci/out/coverage.prof -service=github + go run github.com/mattn/goveralls@latest -coverprofile=ci/out/coverage.prof -service=github gotest: go test -covermode=count -coverprofile=ci/out/coverage.prof -coverpkg=./... $${GOTESTFLAGS-} ./... diff --git a/internal/entryhuman/entry.go b/internal/entryhuman/entry.go index 7d31894..f6df6ec 100644 --- a/internal/entryhuman/entry.go +++ b/internal/entryhuman/entry.go @@ -72,7 +72,7 @@ func Fmt( ts := ent.Time.Format(TimeFormat) buf.WriteString(ts + " ") - level := ent.Level.String() + level := strings.ToLower(ent.Level.String()) if len(level) > 4 { level = level[:4] } @@ -162,7 +162,7 @@ var ( levelDebugStyle = renderer.NewStyle().Foreground(lipgloss.Color("#ffffff")) levelInfoStyle = renderer.NewStyle().Foreground(lipgloss.Color("#0091FF")) levelWarnStyle = renderer.NewStyle().Foreground(lipgloss.Color("#FFCF0D")) - levelErrorStyle = renderer.NewStyle().Foreground(lipgloss.Color("#FF5A0D")) + levelErrorStyle = renderer.NewStyle().Foreground(lipgloss.Color("#FF5A0D")).Bold(true) ) func levelStyle(level slog.Level) lipgloss.Style { diff --git a/map_test.go b/map_test.go index e15a6ee..fce13b5 100644 --- a/map_test.go +++ b/map_test.go @@ -187,7 +187,7 @@ func TestMap(t *testing.T) { t.Parallel() test(t, slog.M( - slog.F("val", time.Date(2000, 02, 05, 4, 4, 4, 0, time.UTC)), + slog.F("val", time.Date(2000, 0o2, 0o5, 4, 4, 4, 0, time.UTC)), ), `{ "val": "2000-02-05T04:04:04Z" }`) @@ -222,10 +222,6 @@ func TestMap(t *testing.T) { }) } -type meow struct { - a int -} - func indentJSON(t *testing.T, j string) string { b := &bytes.Buffer{} err := json.Indent(b, []byte(j), "", strings.Repeat(" ", 4)) diff --git a/sloggers/slogtest/assert/assert.go b/sloggers/slogtest/assert/assert.go index e11476f..1e3c456 100644 --- a/sloggers/slogtest/assert/assert.go +++ b/sloggers/slogtest/assert/assert.go @@ -89,5 +89,4 @@ func stringContainsFold(errs, sub string) bool { sub = strings.ToLower(sub) return strings.Contains(errs, sub) - } diff --git a/sloggers/slogtest/assert/assert_test.go b/sloggers/slogtest/assert/assert_test.go index 49f6c02..b483cf3 100644 --- a/sloggers/slogtest/assert/assert_test.go +++ b/sloggers/slogtest/assert/assert_test.go @@ -44,11 +44,10 @@ func TestErrorContains(t *testing.T) { defer func() { recover() simpleassert.Equal(t, "fatals", 1, tb.fatals) - }() assert.ErrorContains(tb, "meow", io.ErrClosedPipe, "eof") - } + func TestSuccess(t *testing.T) { t.Parallel() From 95d3aece5a5f6d7ced7b1992595368615298b41f Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Mon, 8 May 2023 16:22:11 -0500 Subject: [PATCH 08/16] Try lowercase levels --- internal/entryhuman/testdata/funky.golden | 2 +- internal/entryhuman/testdata/multilineField.golden | 2 +- internal/entryhuman/testdata/multilineMessage.golden | 2 +- internal/entryhuman/testdata/named.golden | 2 +- internal/entryhuman/testdata/simpleNoFields.golden | 2 +- internal/entryhuman/testdata/spacey.golden | 2 +- s_test.go | 2 +- sloggers/sloghuman/sloghuman_test.go | 5 +++-- 8 files changed, 10 insertions(+), 9 deletions(-) diff --git a/internal/entryhuman/testdata/funky.golden b/internal/entryhuman/testdata/funky.golden index f11a873..ab254c0 100644 --- a/internal/entryhuman/testdata/funky.golden +++ b/internal/entryhuman/testdata/funky.golden @@ -1 +1 @@ -0001-01-01 00:00:00.000 [WARN] funky^%&^&^key=value funky^%&^&^key2="@#\t \t \n" \ No newline at end of file +0001-01-01 00:00:00.000 [warn] funky^%&^&^key=value funky^%&^&^key2="@#\t \t \n" \ No newline at end of file diff --git a/internal/entryhuman/testdata/multilineField.golden b/internal/entryhuman/testdata/multilineField.golden index 1a94b70..c691de3 100644 --- a/internal/entryhuman/testdata/multilineField.golden +++ b/internal/entryhuman/testdata/multilineField.golden @@ -1,3 +1,3 @@ -0001-01-01 00:00:00.000 [INFO] msg ... +0001-01-01 00:00:00.000 [info] msg ... field= line1 line2 \ No newline at end of file diff --git a/internal/entryhuman/testdata/multilineMessage.golden b/internal/entryhuman/testdata/multilineMessage.golden index 17e479f..ea1a521 100644 --- a/internal/entryhuman/testdata/multilineMessage.golden +++ b/internal/entryhuman/testdata/multilineMessage.golden @@ -1,3 +1,3 @@ -0001-01-01 00:00:00.000 [INFO] ... +0001-01-01 00:00:00.000 [info] ... msg= line1 line2 \ No newline at end of file diff --git a/internal/entryhuman/testdata/named.golden b/internal/entryhuman/testdata/named.golden index e896ca2..8e92097 100644 --- a/internal/entryhuman/testdata/named.golden +++ b/internal/entryhuman/testdata/named.golden @@ -1 +1 @@ -0001-01-01 00:00:00.000 [WARN] (named.meow) \ No newline at end of file +0001-01-01 00:00:00.000 [warn] (named.meow) \ No newline at end of file diff --git a/internal/entryhuman/testdata/simpleNoFields.golden b/internal/entryhuman/testdata/simpleNoFields.golden index b36ae73..38fce7e 100644 --- a/internal/entryhuman/testdata/simpleNoFields.golden +++ b/internal/entryhuman/testdata/simpleNoFields.golden @@ -1 +1 @@ -2000-02-05 04:04:04.000 [DEBU] wowowow izi \ No newline at end of file +2000-02-05 04:04:04.000 [debu] wowowow izi \ No newline at end of file diff --git a/internal/entryhuman/testdata/spacey.golden b/internal/entryhuman/testdata/spacey.golden index 4a01d24..dbe97ea 100644 --- a/internal/entryhuman/testdata/spacey.golden +++ b/internal/entryhuman/testdata/spacey.golden @@ -1 +1 @@ -0001-01-01 00:00:00.000 [WARN] space_in_my_key="value in my value" \ No newline at end of file +0001-01-01 00:00:00.000 [warn] space_in_my_key="value in my value" \ No newline at end of file diff --git a/s_test.go b/s_test.go index 3914a7d..275783b 100644 --- a/s_test.go +++ b/s_test.go @@ -23,5 +23,5 @@ func TestStdlib(t *testing.T) { et, rest, err := entryhuman.StripTimestamp(b.String()) assert.Success(t, "strip timestamp", err) assert.False(t, "timestamp", et.IsZero()) - assert.Equal(t, "entry", " [INFO]\t(stdlib)\tstdlib\thi=we\n", rest) + assert.Equal(t, "entry", " [info]\t(stdlib)\tstdlib\thi=we\n", rest) } diff --git a/sloggers/sloghuman/sloghuman_test.go b/sloggers/sloghuman/sloghuman_test.go index cd01c89..c763635 100644 --- a/sloggers/sloghuman/sloghuman_test.go +++ b/sloggers/sloghuman/sloghuman_test.go @@ -3,6 +3,7 @@ package sloghuman_test import ( "bytes" "context" + "fmt" "os" "testing" @@ -25,7 +26,7 @@ func TestMake(t *testing.T) { et, rest, err := entryhuman.StripTimestamp(b.String()) assert.Success(t, "strip timestamp", err) assert.False(t, "timestamp", et.IsZero()) - assert.Equal(t, "entry", " [INFO]\t...\twowow=\"me\\nyou\"\n msg= line1\n\n line2\n", rest) + assert.Equal(t, "entry", " [info]\t...\twowow=\"me\\nyou\"\n msg= line1\n\n line2\n", rest) } func TestVisual(t *testing.T) { @@ -39,6 +40,6 @@ func TestVisual(t *testing.T) { l.Info(bg, "line1\n\nline2", slog.F("wowow", "me\nyou")) l.Warn(bg, "oops", slog.F("aaa", "mmm")) l = l.Named("sublogger") - l.Error(bg, "big oops", slog.F("aaa", "mmm")) + l.Error(bg, "big oops", slog.F("aaa", "mmm"), slog.Error(fmt.Errorf("this happened\nand this"))) l.Sync() } From cf7a157a04a5181297c20a9febc90dd55e7c72b7 Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Mon, 8 May 2023 16:38:00 -0500 Subject: [PATCH 09/16] Color keys according to level --- internal/entryhuman/entry.go | 16 ++++++++++------ internal/entryhuman/testdata/funky.golden | 2 +- .../entryhuman/testdata/multilineField.golden | 2 +- .../entryhuman/testdata/multilineMessage.golden | 2 +- internal/entryhuman/testdata/named.golden | 2 +- .../entryhuman/testdata/simpleNoFields.golden | 2 +- internal/entryhuman/testdata/spacey.golden | 2 +- sloggers/sloghuman/sloghuman_test.go | 2 +- 8 files changed, 17 insertions(+), 13 deletions(-) diff --git a/internal/entryhuman/entry.go b/internal/entryhuman/entry.go index f6df6ec..4699d53 100644 --- a/internal/entryhuman/entry.go +++ b/internal/entryhuman/entry.go @@ -36,9 +36,8 @@ const TimeFormat = "2006-01-02 15:04:05.000" var ( renderer = lipgloss.NewRenderer(os.Stdout, termenv.WithUnsafe()) - loggerNameStyle = renderer.NewStyle().Foreground(lipgloss.Color("#A47DFF")) - keyStyle = renderer.NewStyle().Foreground(lipgloss.Color("#606366")) - multiLineKeyStyle = renderer.NewStyle().Foreground(lipgloss.Color("#79b8ff")) + loggerNameStyle = renderer.NewStyle().Foreground(lipgloss.Color("#A47DFF")) + timeStyle = renderer.NewStyle().Foreground(lipgloss.Color("#606366")) ) func render(w io.Writer, st lipgloss.Style, s string) string { @@ -70,9 +69,9 @@ func Fmt( ) { reset(buf, termW) ts := ent.Time.Format(TimeFormat) - buf.WriteString(ts + " ") + buf.WriteString(render(termW, timeStyle, ts+" ")) - level := strings.ToLower(ent.Level.String()) + level := ent.Level.String() if len(level) > 4 { level = level[:4] } @@ -127,6 +126,11 @@ func Fmt( multilineVal = s } + // Basic keyStyle off of the level makes it easy to distinguish individual + // entries in a fast stream of logs where some are multi-line. + // See logrus for an example. + keyStyle := levelStyle(ent.Level).Copy().Bold(false) + for i, f := range ent.Fields { if i < len(ent.Fields) { buf.WriteString("\t") @@ -150,7 +154,7 @@ func Fmt( } multilineVal = strings.Join(lines, "\n") - multilineKey = render(termW, multiLineKeyStyle, multilineKey) + multilineKey = render(termW, keyStyle, multilineKey) buf.WriteString("\n") buf.WriteString(multilineKey) buf.WriteString("= ") diff --git a/internal/entryhuman/testdata/funky.golden b/internal/entryhuman/testdata/funky.golden index ab254c0..f11a873 100644 --- a/internal/entryhuman/testdata/funky.golden +++ b/internal/entryhuman/testdata/funky.golden @@ -1 +1 @@ -0001-01-01 00:00:00.000 [warn] funky^%&^&^key=value funky^%&^&^key2="@#\t \t \n" \ No newline at end of file +0001-01-01 00:00:00.000 [WARN] funky^%&^&^key=value funky^%&^&^key2="@#\t \t \n" \ No newline at end of file diff --git a/internal/entryhuman/testdata/multilineField.golden b/internal/entryhuman/testdata/multilineField.golden index c691de3..1a94b70 100644 --- a/internal/entryhuman/testdata/multilineField.golden +++ b/internal/entryhuman/testdata/multilineField.golden @@ -1,3 +1,3 @@ -0001-01-01 00:00:00.000 [info] msg ... +0001-01-01 00:00:00.000 [INFO] msg ... field= line1 line2 \ No newline at end of file diff --git a/internal/entryhuman/testdata/multilineMessage.golden b/internal/entryhuman/testdata/multilineMessage.golden index ea1a521..17e479f 100644 --- a/internal/entryhuman/testdata/multilineMessage.golden +++ b/internal/entryhuman/testdata/multilineMessage.golden @@ -1,3 +1,3 @@ -0001-01-01 00:00:00.000 [info] ... +0001-01-01 00:00:00.000 [INFO] ... msg= line1 line2 \ No newline at end of file diff --git a/internal/entryhuman/testdata/named.golden b/internal/entryhuman/testdata/named.golden index 8e92097..e896ca2 100644 --- a/internal/entryhuman/testdata/named.golden +++ b/internal/entryhuman/testdata/named.golden @@ -1 +1 @@ -0001-01-01 00:00:00.000 [warn] (named.meow) \ No newline at end of file +0001-01-01 00:00:00.000 [WARN] (named.meow) \ No newline at end of file diff --git a/internal/entryhuman/testdata/simpleNoFields.golden b/internal/entryhuman/testdata/simpleNoFields.golden index 38fce7e..b36ae73 100644 --- a/internal/entryhuman/testdata/simpleNoFields.golden +++ b/internal/entryhuman/testdata/simpleNoFields.golden @@ -1 +1 @@ -2000-02-05 04:04:04.000 [debu] wowowow izi \ No newline at end of file +2000-02-05 04:04:04.000 [DEBU] wowowow izi \ No newline at end of file diff --git a/internal/entryhuman/testdata/spacey.golden b/internal/entryhuman/testdata/spacey.golden index dbe97ea..4a01d24 100644 --- a/internal/entryhuman/testdata/spacey.golden +++ b/internal/entryhuman/testdata/spacey.golden @@ -1 +1 @@ -0001-01-01 00:00:00.000 [warn] space_in_my_key="value in my value" \ No newline at end of file +0001-01-01 00:00:00.000 [WARN] space_in_my_key="value in my value" \ No newline at end of file diff --git a/sloggers/sloghuman/sloghuman_test.go b/sloggers/sloghuman/sloghuman_test.go index c763635..5ae5828 100644 --- a/sloggers/sloghuman/sloghuman_test.go +++ b/sloggers/sloghuman/sloghuman_test.go @@ -26,7 +26,7 @@ func TestMake(t *testing.T) { et, rest, err := entryhuman.StripTimestamp(b.String()) assert.Success(t, "strip timestamp", err) assert.False(t, "timestamp", et.IsZero()) - assert.Equal(t, "entry", " [info]\t...\twowow=\"me\\nyou\"\n msg= line1\n\n line2\n", rest) + assert.Equal(t, "entry", " [INFO]\t...\twowow=\"me\\nyou\"\n msg= line1\n\n line2\n", rest) } func TestVisual(t *testing.T) { From 0c5e0972fc2d740e61149dfd82b498ec6025d766 Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Mon, 8 May 2023 16:43:43 -0500 Subject: [PATCH 10/16] Lowercase, again --- internal/entryhuman/entry.go | 1 + internal/entryhuman/testdata/funky.golden | 2 +- internal/entryhuman/testdata/multilineField.golden | 2 +- internal/entryhuman/testdata/multilineMessage.golden | 2 +- internal/entryhuman/testdata/named.golden | 2 +- internal/entryhuman/testdata/simpleNoFields.golden | 2 +- internal/entryhuman/testdata/spacey.golden | 2 +- sloggers/sloghuman/sloghuman_test.go | 2 +- 8 files changed, 8 insertions(+), 7 deletions(-) diff --git a/internal/entryhuman/entry.go b/internal/entryhuman/entry.go index 4699d53..d422739 100644 --- a/internal/entryhuman/entry.go +++ b/internal/entryhuman/entry.go @@ -72,6 +72,7 @@ func Fmt( buf.WriteString(render(termW, timeStyle, ts+" ")) level := ent.Level.String() + level = strings.ToLower(level) if len(level) > 4 { level = level[:4] } diff --git a/internal/entryhuman/testdata/funky.golden b/internal/entryhuman/testdata/funky.golden index f11a873..ab254c0 100644 --- a/internal/entryhuman/testdata/funky.golden +++ b/internal/entryhuman/testdata/funky.golden @@ -1 +1 @@ -0001-01-01 00:00:00.000 [WARN] funky^%&^&^key=value funky^%&^&^key2="@#\t \t \n" \ No newline at end of file +0001-01-01 00:00:00.000 [warn] funky^%&^&^key=value funky^%&^&^key2="@#\t \t \n" \ No newline at end of file diff --git a/internal/entryhuman/testdata/multilineField.golden b/internal/entryhuman/testdata/multilineField.golden index 1a94b70..c691de3 100644 --- a/internal/entryhuman/testdata/multilineField.golden +++ b/internal/entryhuman/testdata/multilineField.golden @@ -1,3 +1,3 @@ -0001-01-01 00:00:00.000 [INFO] msg ... +0001-01-01 00:00:00.000 [info] msg ... field= line1 line2 \ No newline at end of file diff --git a/internal/entryhuman/testdata/multilineMessage.golden b/internal/entryhuman/testdata/multilineMessage.golden index 17e479f..ea1a521 100644 --- a/internal/entryhuman/testdata/multilineMessage.golden +++ b/internal/entryhuman/testdata/multilineMessage.golden @@ -1,3 +1,3 @@ -0001-01-01 00:00:00.000 [INFO] ... +0001-01-01 00:00:00.000 [info] ... msg= line1 line2 \ No newline at end of file diff --git a/internal/entryhuman/testdata/named.golden b/internal/entryhuman/testdata/named.golden index e896ca2..8e92097 100644 --- a/internal/entryhuman/testdata/named.golden +++ b/internal/entryhuman/testdata/named.golden @@ -1 +1 @@ -0001-01-01 00:00:00.000 [WARN] (named.meow) \ No newline at end of file +0001-01-01 00:00:00.000 [warn] (named.meow) \ No newline at end of file diff --git a/internal/entryhuman/testdata/simpleNoFields.golden b/internal/entryhuman/testdata/simpleNoFields.golden index b36ae73..38fce7e 100644 --- a/internal/entryhuman/testdata/simpleNoFields.golden +++ b/internal/entryhuman/testdata/simpleNoFields.golden @@ -1 +1 @@ -2000-02-05 04:04:04.000 [DEBU] wowowow izi \ No newline at end of file +2000-02-05 04:04:04.000 [debu] wowowow izi \ No newline at end of file diff --git a/internal/entryhuman/testdata/spacey.golden b/internal/entryhuman/testdata/spacey.golden index 4a01d24..dbe97ea 100644 --- a/internal/entryhuman/testdata/spacey.golden +++ b/internal/entryhuman/testdata/spacey.golden @@ -1 +1 @@ -0001-01-01 00:00:00.000 [WARN] space_in_my_key="value in my value" \ No newline at end of file +0001-01-01 00:00:00.000 [warn] space_in_my_key="value in my value" \ No newline at end of file diff --git a/sloggers/sloghuman/sloghuman_test.go b/sloggers/sloghuman/sloghuman_test.go index 5ae5828..c763635 100644 --- a/sloggers/sloghuman/sloghuman_test.go +++ b/sloggers/sloghuman/sloghuman_test.go @@ -26,7 +26,7 @@ func TestMake(t *testing.T) { et, rest, err := entryhuman.StripTimestamp(b.String()) assert.Success(t, "strip timestamp", err) assert.False(t, "timestamp", et.IsZero()) - assert.Equal(t, "entry", " [INFO]\t...\twowow=\"me\\nyou\"\n msg= line1\n\n line2\n", rest) + assert.Equal(t, "entry", " [info]\t...\twowow=\"me\\nyou\"\n msg= line1\n\n line2\n", rest) } func TestVisual(t *testing.T) { From 041c0a5a29beb8ce2cb1c1040e43256e083cfa78 Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Mon, 8 May 2023 17:01:58 -0500 Subject: [PATCH 11/16] Improve buffering in sloghuman --- internal/entryhuman/entry.go | 4 ++- sloggers/sloghuman/sloghuman.go | 42 +++++++++++++++++++--------- sloggers/sloghuman/sloghuman_test.go | 2 +- 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/internal/entryhuman/entry.go b/internal/entryhuman/entry.go index d422739..43b066f 100644 --- a/internal/entryhuman/entry.go +++ b/internal/entryhuman/entry.go @@ -131,12 +131,14 @@ func Fmt( // entries in a fast stream of logs where some are multi-line. // See logrus for an example. keyStyle := levelStyle(ent.Level).Copy().Bold(false) + equalsStyle := levelStyle(ent.Level).Copy().Faint(true) for i, f := range ent.Fields { if i < len(ent.Fields) { buf.WriteString("\t") } - buf.WriteString(render(termW, keyStyle, quoteKey(f.Name)+"=")) + buf.WriteString(render(termW, keyStyle, quoteKey(f.Name))) + buf.WriteString(render(termW, equalsStyle, "=")) valueStr := fmt.Sprintf("%+v", f.Value) buf.WriteString(quote(valueStr)) } diff --git a/sloggers/sloghuman/sloghuman.go b/sloggers/sloghuman/sloghuman.go index ebd6a01..5247d17 100644 --- a/sloggers/sloghuman/sloghuman.go +++ b/sloggers/sloghuman/sloghuman.go @@ -3,9 +3,11 @@ package sloghuman // import "cdr.dev/slog/sloggers/sloghuman" import ( + "bufio" + "bytes" "context" "io" - "strings" + "sync" "cdr.dev/slog" "cdr.dev/slog/internal/entryhuman" @@ -29,26 +31,40 @@ type humanSink struct { w2 io.Writer } +var bufPool = sync.Pool{ + New: func() interface{} { + return bytes.NewBuffer(make([]byte, 0, 256)) + }, +} + func (s humanSink) LogEntry(ctx context.Context, ent slog.SinkEntry) { - var sb strings.Builder - entryhuman.Fmt(&sb, s.w2, ent) - str := sb.String() - lines := strings.Split(str, "\n") + buf1 := bufPool.Get().(*bytes.Buffer) + buf1.Reset() + defer bufPool.Put(buf1) + + buf2 := bufPool.Get().(*bytes.Buffer) + buf2.Reset() + defer bufPool.Put(buf2) + + entryhuman.Fmt(buf1, s.w2, ent) + + var ( + i int + sc = bufio.NewScanner(buf1) + ) // We need to add 4 spaces before every field line for readability. // humanfmt doesn't do it for us because the testSink doesn't want // it as *testing.T automatically does it. - fieldsLines := lines[1:] - for i, line := range fieldsLines { - if line == "" { - continue + for ; sc.Scan(); i++ { + if i > 0 && len(sc.Bytes()) > 0 { + buf2.Write([]byte(" ")) } - fieldsLines[i] = strings.Repeat(" ", 2) + line + buf2.Write(sc.Bytes()) + buf2.WriteByte('\n') } - str = strings.Join(lines, "\n") - - s.w.Write("sloghuman", []byte(str+"\n")) + s.w.Write("sloghuman", buf2.Bytes()) } func (s humanSink) Sync() { diff --git a/sloggers/sloghuman/sloghuman_test.go b/sloggers/sloghuman/sloghuman_test.go index c763635..2b15f30 100644 --- a/sloggers/sloghuman/sloghuman_test.go +++ b/sloggers/sloghuman/sloghuman_test.go @@ -26,7 +26,7 @@ func TestMake(t *testing.T) { et, rest, err := entryhuman.StripTimestamp(b.String()) assert.Success(t, "strip timestamp", err) assert.False(t, "timestamp", et.IsZero()) - assert.Equal(t, "entry", " [info]\t...\twowow=\"me\\nyou\"\n msg= line1\n\n line2\n", rest) + assert.Equal(t, "entry", " [info]\t...\twowow=\"me\\nyou\"\n msg= line1\n\n line2\n", rest) } func TestVisual(t *testing.T) { From 08be77ee712d747fe743b40be8e46432b11627e6 Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Mon, 8 May 2023 17:06:19 -0500 Subject: [PATCH 12/16] Calm down colors --- internal/entryhuman/entry.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/internal/entryhuman/entry.go b/internal/entryhuman/entry.go index 43b066f..7adc6c3 100644 --- a/internal/entryhuman/entry.go +++ b/internal/entryhuman/entry.go @@ -127,11 +127,9 @@ func Fmt( multilineVal = s } - // Basic keyStyle off of the level makes it easy to distinguish individual - // entries in a fast stream of logs where some are multi-line. - // See logrus for an example. - keyStyle := levelStyle(ent.Level).Copy().Bold(false) - equalsStyle := levelStyle(ent.Level).Copy().Faint(true) + keyStyle := timeStyle.Copy() + // Help users distinguish logs by keeping some color in the equal signs. + equalsStyle := levelStyle(ent.Level) for i, f := range ent.Fields { if i < len(ent.Fields) { @@ -169,7 +167,7 @@ var ( levelDebugStyle = renderer.NewStyle().Foreground(lipgloss.Color("#ffffff")) levelInfoStyle = renderer.NewStyle().Foreground(lipgloss.Color("#0091FF")) levelWarnStyle = renderer.NewStyle().Foreground(lipgloss.Color("#FFCF0D")) - levelErrorStyle = renderer.NewStyle().Foreground(lipgloss.Color("#FF5A0D")).Bold(true) + levelErrorStyle = renderer.NewStyle().Foreground(lipgloss.Color("#FF5A0D")) ) func levelStyle(level slog.Level) lipgloss.Style { From 677a5aeb1b096e32da4e2309131d24d7a9a152f8 Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Mon, 8 May 2023 20:13:29 -0500 Subject: [PATCH 13/16] Remove coloring from equal sign --- internal/entryhuman/entry.go | 14 ++++++++------ sloggers/sloghuman/sloghuman_test.go | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/internal/entryhuman/entry.go b/internal/entryhuman/entry.go index 7adc6c3..ad1fa15 100644 --- a/internal/entryhuman/entry.go +++ b/internal/entryhuman/entry.go @@ -54,6 +54,8 @@ func reset(w io.Writer, termW io.Writer) { } } +const tab = " " + // Fmt returns a human readable format for ent. // // We never return with a trailing newline because Go's testing framework adds one @@ -78,12 +80,12 @@ func Fmt( } level = "[" + level + "]" buf.WriteString(render(termW, levelStyle(ent.Level), level)) - buf.WriteString("\t") + buf.WriteString(" ") if len(ent.LoggerNames) > 0 { loggerName := "(" + quoteKey(strings.Join(ent.LoggerNames, ".")) + ")" buf.WriteString(render(termW, loggerNameStyle, loggerName)) - buf.WriteString("\t") + buf.WriteString(tab) } var multilineKey string @@ -127,13 +129,13 @@ func Fmt( multilineVal = s } - keyStyle := timeStyle.Copy() + keyStyle := timeStyle // Help users distinguish logs by keeping some color in the equal signs. - equalsStyle := levelStyle(ent.Level) + equalsStyle := timeStyle for i, f := range ent.Fields { if i < len(ent.Fields) { - buf.WriteString("\t") + buf.WriteString(tab) } buf.WriteString(render(termW, keyStyle, quoteKey(f.Name))) buf.WriteString(render(termW, equalsStyle, "=")) @@ -164,7 +166,7 @@ func Fmt( } var ( - levelDebugStyle = renderer.NewStyle().Foreground(lipgloss.Color("#ffffff")) + levelDebugStyle = timeStyle.Copy() levelInfoStyle = renderer.NewStyle().Foreground(lipgloss.Color("#0091FF")) levelWarnStyle = renderer.NewStyle().Foreground(lipgloss.Color("#FFCF0D")) levelErrorStyle = renderer.NewStyle().Foreground(lipgloss.Color("#FF5A0D")) diff --git a/sloggers/sloghuman/sloghuman_test.go b/sloggers/sloghuman/sloghuman_test.go index 2b15f30..9047161 100644 --- a/sloggers/sloghuman/sloghuman_test.go +++ b/sloggers/sloghuman/sloghuman_test.go @@ -26,7 +26,7 @@ func TestMake(t *testing.T) { et, rest, err := entryhuman.StripTimestamp(b.String()) assert.Success(t, "strip timestamp", err) assert.False(t, "timestamp", et.IsZero()) - assert.Equal(t, "entry", " [info]\t...\twowow=\"me\\nyou\"\n msg= line1\n\n line2\n", rest) + assert.Equal(t, "entry", " [info] ... wowow=\"me\\nyou\"\n msg= line1\n\n line2\n", rest) } func TestVisual(t *testing.T) { From fbe4576b00f69249003b49bce9f7680d6ea3e6df Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Mon, 8 May 2023 20:33:23 -0500 Subject: [PATCH 14/16] Format objects more nicely --- internal/entryhuman/entry.go | 26 +++++++++++- internal/entryhuman/entry_test.go | 41 ++++++++++++++++++- internal/entryhuman/testdata/bytes.golden | 1 + internal/entryhuman/testdata/funky.golden | 2 +- .../entryhuman/testdata/multilineField.golden | 2 +- .../testdata/multilineMessage.golden | 2 +- internal/entryhuman/testdata/named.golden | 2 +- internal/entryhuman/testdata/object.golden | 1 + .../entryhuman/testdata/simpleNoFields.golden | 2 +- internal/entryhuman/testdata/spacey.golden | 2 +- s_test.go | 2 +- 11 files changed, 72 insertions(+), 11 deletions(-) create mode 100644 internal/entryhuman/testdata/bytes.golden create mode 100644 internal/entryhuman/testdata/object.golden diff --git a/internal/entryhuman/entry.go b/internal/entryhuman/entry.go index ad1fa15..6c8406d 100644 --- a/internal/entryhuman/entry.go +++ b/internal/entryhuman/entry.go @@ -4,9 +4,11 @@ package entryhuman import ( "bytes" + "encoding/json" "fmt" "io" "os" + "reflect" "strconv" "strings" "time" @@ -54,6 +56,26 @@ func reset(w io.Writer, termW io.Writer) { } } +func formatValue(v interface{}) string { + typ := reflect.TypeOf(v) + switch typ.Kind() { + case reflect.Struct, reflect.Map: + byt, err := json.Marshal(v) + if err != nil { + panic(err) + } + return string(byt) + case reflect.Slice: + // Byte slices are optimistically readable. + if typ.Elem().Kind() == reflect.Uint8 { + return fmt.Sprintf("%q", v) + } + fallthrough + default: + return quote(fmt.Sprintf("%+v", v)) + } +} + const tab = " " // Fmt returns a human readable format for ent. @@ -139,8 +161,8 @@ func Fmt( } buf.WriteString(render(termW, keyStyle, quoteKey(f.Name))) buf.WriteString(render(termW, equalsStyle, "=")) - valueStr := fmt.Sprintf("%+v", f.Value) - buf.WriteString(quote(valueStr)) + valueStr := formatValue(f.Value) + buf.WriteString(valueStr) } if multilineVal != "" { diff --git a/internal/entryhuman/entry_test.go b/internal/entryhuman/entry_test.go index 1da603d..6c62827 100644 --- a/internal/entryhuman/entry_test.go +++ b/internal/entryhuman/entry_test.go @@ -5,7 +5,6 @@ import ( "flag" "fmt" "io" - "io/ioutil" "os" "testing" "time" @@ -19,6 +18,12 @@ var kt = time.Date(2000, time.February, 5, 4, 4, 4, 4, time.UTC) var updateGoldenFiles = flag.Bool("update-golden-files", false, "update golden files in testdata") +type testObj struct { + foo int + bar int + dra []byte +} + func TestEntry(t *testing.T) { t.Parallel() @@ -81,6 +86,38 @@ func TestEntry(t *testing.T) { ), }, }, + { + "bytes", + slog.SinkEntry{ + Level: slog.LevelWarn, + Fields: slog.M( + slog.F("somefile", []byte("blah bla\x01h blah")), + ), + }, + }, + { + "object", + slog.SinkEntry{ + Level: slog.LevelWarn, + Fields: slog.M( + slog.F("obj", slog.M( + slog.F("obj1", testObj{ + foo: 1, + bar: 2, + dra: []byte("blah"), + }), + slog.F("obj2", testObj{ + foo: 3, + bar: 4, + dra: []byte("blah"), + }), + )), + slog.F("map", map[string]string{ + "key1": "value1", + }), + ), + }, + }, } if *updateGoldenFiles { ents, err := os.ReadDir("testdata") @@ -99,7 +136,7 @@ func TestEntry(t *testing.T) { goldenPath := fmt.Sprintf("testdata/%s.golden", tc.name) var gotBuf bytes.Buffer - entryhuman.Fmt(&gotBuf, ioutil.Discard, tc.ent) + entryhuman.Fmt(&gotBuf, io.Discard, tc.ent) if *updateGoldenFiles { err := os.WriteFile(goldenPath, gotBuf.Bytes(), 0o644) diff --git a/internal/entryhuman/testdata/bytes.golden b/internal/entryhuman/testdata/bytes.golden new file mode 100644 index 0000000..e4c5490 --- /dev/null +++ b/internal/entryhuman/testdata/bytes.golden @@ -0,0 +1 @@ +0001-01-01 00:00:00.000 [warn] somefile="blah bla\x01h blah" \ No newline at end of file diff --git a/internal/entryhuman/testdata/funky.golden b/internal/entryhuman/testdata/funky.golden index ab254c0..fc6a460 100644 --- a/internal/entryhuman/testdata/funky.golden +++ b/internal/entryhuman/testdata/funky.golden @@ -1 +1 @@ -0001-01-01 00:00:00.000 [warn] funky^%&^&^key=value funky^%&^&^key2="@#\t \t \n" \ No newline at end of file +0001-01-01 00:00:00.000 [warn] funky^%&^&^key=value funky^%&^&^key2="@#\t \t \n" \ No newline at end of file diff --git a/internal/entryhuman/testdata/multilineField.golden b/internal/entryhuman/testdata/multilineField.golden index c691de3..a2777d8 100644 --- a/internal/entryhuman/testdata/multilineField.golden +++ b/internal/entryhuman/testdata/multilineField.golden @@ -1,3 +1,3 @@ -0001-01-01 00:00:00.000 [info] msg ... +0001-01-01 00:00:00.000 [info] msg ... field= line1 line2 \ No newline at end of file diff --git a/internal/entryhuman/testdata/multilineMessage.golden b/internal/entryhuman/testdata/multilineMessage.golden index ea1a521..233fda6 100644 --- a/internal/entryhuman/testdata/multilineMessage.golden +++ b/internal/entryhuman/testdata/multilineMessage.golden @@ -1,3 +1,3 @@ -0001-01-01 00:00:00.000 [info] ... +0001-01-01 00:00:00.000 [info] ... msg= line1 line2 \ No newline at end of file diff --git a/internal/entryhuman/testdata/named.golden b/internal/entryhuman/testdata/named.golden index 8e92097..09efb6e 100644 --- a/internal/entryhuman/testdata/named.golden +++ b/internal/entryhuman/testdata/named.golden @@ -1 +1 @@ -0001-01-01 00:00:00.000 [warn] (named.meow) \ No newline at end of file +0001-01-01 00:00:00.000 [warn] (named.meow) \ No newline at end of file diff --git a/internal/entryhuman/testdata/object.golden b/internal/entryhuman/testdata/object.golden new file mode 100644 index 0000000..855cb06 --- /dev/null +++ b/internal/entryhuman/testdata/object.golden @@ -0,0 +1 @@ +0001-01-01 00:00:00.000 [warn] obj="[{Name:obj1 Value:{foo:1 bar:2 dra:[98 108 97 104]}} {Name:obj2 Value:{foo:3 bar:4 dra:[98 108 97 104]}}]" map={"key1":"value1"} \ No newline at end of file diff --git a/internal/entryhuman/testdata/simpleNoFields.golden b/internal/entryhuman/testdata/simpleNoFields.golden index 38fce7e..db46f6a 100644 --- a/internal/entryhuman/testdata/simpleNoFields.golden +++ b/internal/entryhuman/testdata/simpleNoFields.golden @@ -1 +1 @@ -2000-02-05 04:04:04.000 [debu] wowowow izi \ No newline at end of file +2000-02-05 04:04:04.000 [debu] wowowow izi \ No newline at end of file diff --git a/internal/entryhuman/testdata/spacey.golden b/internal/entryhuman/testdata/spacey.golden index dbe97ea..7135d8c 100644 --- a/internal/entryhuman/testdata/spacey.golden +++ b/internal/entryhuman/testdata/spacey.golden @@ -1 +1 @@ -0001-01-01 00:00:00.000 [warn] space_in_my_key="value in my value" \ No newline at end of file +0001-01-01 00:00:00.000 [warn] space_in_my_key="value in my value" \ No newline at end of file diff --git a/s_test.go b/s_test.go index 275783b..19578c9 100644 --- a/s_test.go +++ b/s_test.go @@ -23,5 +23,5 @@ func TestStdlib(t *testing.T) { et, rest, err := entryhuman.StripTimestamp(b.String()) assert.Success(t, "strip timestamp", err) assert.False(t, "timestamp", et.IsZero()) - assert.Equal(t, "entry", " [info]\t(stdlib)\tstdlib\thi=we\n", rest) + assert.Equal(t, "entry", " [info] (stdlib) stdlib hi=we\n", rest) } From c007029be0b08f3eb3680ba009dc15ae7e4369c7 Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Mon, 8 May 2023 21:37:55 -0500 Subject: [PATCH 15/16] play with logfmt --- internal/entryhuman/logfmt/encoder.go | 118 ++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 internal/entryhuman/logfmt/encoder.go diff --git a/internal/entryhuman/logfmt/encoder.go b/internal/entryhuman/logfmt/encoder.go new file mode 100644 index 0000000..98d9d18 --- /dev/null +++ b/internal/entryhuman/logfmt/encoder.go @@ -0,0 +1,118 @@ +package logfmt + +import ( + "fmt" + "io" + "reflect" + "strconv" + "unicode" +) + +type Encoder struct { + w io.Writer + FormatKey func(key string) string + // FormatPrimitiveValue is used to format primitive values (strings, ints, + // floats, etc). It is not used for arrays or objects. + FormatPrimitiveValue func(value interface{}) string +} + +func NewEncoder(w io.Writer) *Encoder { + return &Encoder{ + FormatKey: func(key string) string { return key }, + FormatPrimitiveValue: func(value interface{}) string { return fmt.Sprintf("%+v", value) }, + w: w, + } +} + +func isPrimitive(typ reflect.Type) bool { + switch typ.Kind() { + case reflect.Bool: + return true + case reflect.String: + return true + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return true + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return true + case reflect.Float32, reflect.Float64: + return true + case reflect.Complex64, reflect.Complex128: + return true + default: + return false + } +} + +// Encode encodes the given message to the writer. For flat objects, the +// output resembles key=value pairs. For nested objects, a surrounding { } is +// used. For arrays, a surrounding [ ] is used. +func (e *Encoder) Encode(m interface{}) error { + typ := reflect.TypeOf(m) + if typ.Kind() == reflect.Ptr { + typ = typ.Elem() + } + + if isPrimitive(typ) { + e.w.Write([]byte(e.FormatPrimitiveValue(m))) + return nil + } + + switch typ.Kind() { + case reflect.Struct: + v := reflect.ValueOf(m) + for i := 0; i < typ.NumField(); i++ { + field := typ.Field(i) + value := v.Field(i) + if !value.CanInterface() { + continue + } + if value.IsZero() { + continue + } + if field.Anonymous { + if err := e.Encode(value.Interface()); err != nil { + return err + } + continue + } + if e.FormatKey != nil { + e.w.Write([]byte(e.FormatKey(field.Name))) + } else { + e.w.Write([]byte(field.Name)) + } + e.w.Write([]byte("=")) + if e.FormatPrimitiveValue != nil { + e.w.Write([]byte(e.FormatPrimitiveValue(value.Interface()))) + } else { + e.w.Write([]byte(formatValue(value.Interface()))) + } + default: + return fmt.Errorf("unsupported type %T", m) + } +} + +// quotes quotes a string so that it is suitable +// as a key for a map or in general some output that +// cannot span multiple lines or have weird characters. +func quote(key string) string { + // strconv.Quote does not quote an empty string so we need this. + if key == "" { + return `""` + } + + var hasSpace bool + for _, r := range key { + if unicode.IsSpace(r) { + hasSpace = true + break + } + } + quoted := strconv.Quote(key) + // If the key doesn't need to be quoted, don't quote it. + // We do not use strconv.CanBackquote because it doesn't + // account tabs. + if !hasSpace && quoted[1:len(quoted)-1] == key { + return key + } + return quoted +} From cd77569d26c674ad0c4fe045211b992a82f34546 Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Tue, 9 May 2023 12:51:34 -0500 Subject: [PATCH 16/16] some docs --- internal/entryhuman/logfmt/README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 internal/entryhuman/logfmt/README.md diff --git a/internal/entryhuman/logfmt/README.md b/internal/entryhuman/logfmt/README.md new file mode 100644 index 0000000..8d2cd17 --- /dev/null +++ b/internal/entryhuman/logfmt/README.md @@ -0,0 +1,25 @@ +# logfmt + +logfmt provides an implementation that supports nested objects and arrays. It +is meant to be a drop-in replacement for JSON, which other logfmt implements +do not support. + +This package makes the trade-off of being more difficult for computers to parse +in favor of the human. + +See these examples: + +JSON: +```json +{ "user": { "id": 123, "name": "foo", "age": 20, "hobbies": ["basketball", "football"] } } +``` + +flat logfmt: +``` +user.id=123 user.name=foo user.age=20 user.hobbies="basketball,football" +``` + +nested logfmt: +``` +user={ id=123 name=foo age=20 hobbies=[basketball football] } +``` pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy