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] } +```