@@ -4,17 +4,15 @@ package entryhuman
4
4
5
5
import (
6
6
"bytes"
7
- "encoding/json"
8
7
"fmt"
9
8
"io"
10
9
"os"
11
- "path/filepath"
12
- "runtime/debug"
13
10
"strconv"
14
11
"strings"
15
12
"time"
16
13
17
- "github.com/fatih/color"
14
+ "github.com/charmbracelet/lipgloss"
15
+ "github.com/muesli/termenv"
18
16
"go.opencensus.io/trace"
19
17
"golang.org/x/crypto/ssh/terminal"
20
18
"golang.org/x/xerrors"
@@ -34,13 +32,20 @@ func StripTimestamp(ent string) (time.Time, string, error) {
34
32
// TimeFormat is a simplified RFC3339 format.
35
33
const TimeFormat = "2006-01-02 15:04:05.000"
36
34
37
- func c (w io.Writer , attrs ... color.Attribute ) * color.Color {
38
- c := color .New (attrs ... )
39
- c .DisableColor ()
35
+ var (
36
+ renderer = lipgloss .NewRenderer (os .Stdout , termenv .WithUnsafe ())
37
+
38
+ loggerNameStyle = renderer .NewStyle ().Foreground (lipgloss .Color ("#A47DFF" ))
39
+ keyStyle = renderer .NewStyle ().Foreground (lipgloss .Color ("#606366" ))
40
+ multiLineKeyStyle = renderer .NewStyle ().Foreground (lipgloss .Color ("#79b8ff" ))
41
+ )
42
+
43
+ func render (w io.Writer , st lipgloss.Style , s string ) string {
40
44
if shouldColor (w ) {
41
- c .EnableColor ()
45
+ ss := st .Render (s )
46
+ return ss
42
47
}
43
- return c
48
+ return s
44
49
}
45
50
46
51
// Fmt returns a human readable format for ent.
@@ -50,36 +55,36 @@ func c(w io.Writer, attrs ...color.Attribute) *color.Color {
50
55
// We also do not indent the fields as go's test does that automatically
51
56
// for extra lines in a log so if we did it here, the fields would be indented
52
57
// twice in test logs. So the Stderr logger indents all the fields itself.
53
- func Fmt (w io.Writer , ent slog.SinkEntry ) string {
54
- ents := c ( w , color . Reset ). Sprint ( "" )
58
+ func Fmt (buf io.StringWriter , termW io. Writer , ent slog.SinkEntry ,
59
+ ) {
55
60
ts := ent .Time .Format (TimeFormat )
56
- ents += ts + " "
61
+ buf . WriteString ( ts + " " )
57
62
58
- level := "[" + ent .Level .String () + "]"
59
- level = c (w , levelColor (ent .Level )).Sprint (level )
60
- ents += fmt .Sprintf ("%v\t " , level )
63
+ level := ent .Level .String ()
64
+ if len (level ) > 4 {
65
+ level = level [:4 ]
66
+ }
67
+ level = "[" + level + "]"
68
+ buf .WriteString (render (termW , levelStyle (ent .Level ), level ))
69
+ buf .WriteString ("\t " )
61
70
62
71
if len (ent .LoggerNames ) > 0 {
63
72
loggerName := "(" + quoteKey (strings .Join (ent .LoggerNames , "." )) + ")"
64
- loggerName = c ( w , color . FgMagenta ). Sprint ( loggerName )
65
- ents += fmt . Sprintf ( "%v \t ", loggerName )
73
+ buf . WriteString ( render ( termW , loggerNameStyle , loggerName ) )
74
+ buf . WriteString ( " \t " )
66
75
}
67
76
68
- hpath , hfn := humanPathAndFunc (ent .File , ent .Func )
69
- loc := fmt .Sprintf ("<%v:%v>\t %v" , hpath , ent .Line , hfn )
70
- loc = c (w , color .FgCyan ).Sprint (loc )
71
- ents += fmt .Sprintf ("%v\t " , loc )
72
-
73
77
var multilineKey string
74
78
var multilineVal string
75
79
msg := strings .TrimSpace (ent .Message )
76
80
if strings .Contains (msg , "\n " ) {
77
81
multilineKey = "msg"
78
82
multilineVal = msg
79
83
msg = "..."
84
+ msg = quote (msg )
85
+ buf .WriteString (msg )
86
+
80
87
}
81
- msg = quote (msg )
82
- ents += msg
83
88
84
89
if ent .SpanContext != (trace.SpanContext {}) {
85
90
ent .Fields = append (slog .M (
@@ -111,48 +116,56 @@ func Fmt(w io.Writer, ent slog.SinkEntry) string {
111
116
multilineVal = s
112
117
}
113
118
114
- if len ( ent .Fields ) > 0 {
115
- // No error is guaranteed due to slog.Map handling errors itself.
116
- fields , _ := json . MarshalIndent ( ent . Fields , "" , " " )
117
- fields = bytes . ReplaceAll ( fields , [] byte ( ", \n " ), [] byte ( ", " ))
118
- fields = bytes . ReplaceAll ( fields , [] byte ( " \n " ), [] byte ( " " ))
119
- fields = formatJSON ( w , fields )
120
- ents += " \t " + string ( fields )
119
+ for i , f := range ent .Fields {
120
+ if i < len ( ent . Fields ) {
121
+ buf . WriteString ( " \t " )
122
+ }
123
+ buf . WriteString ( render ( termW , keyStyle , f . Name + "= " ))
124
+ valueStr := fmt . Sprintf ( "%+v" , f . Value )
125
+ buf . WriteString ( quote ( valueStr ) )
121
126
}
122
127
123
128
if multilineVal != "" {
124
129
if msg != "..." {
125
- ents += " ..."
130
+ buf . WriteString ( " ..." )
126
131
}
127
132
128
133
// Proper indentation.
129
134
lines := strings .Split (multilineVal , "\n " )
130
135
for i , line := range lines [1 :] {
131
136
if line != "" {
132
- lines [i + 1 ] = c ( w , color . Reset ). Sprint ( "" ) + strings .Repeat (" " , len (multilineKey )+ 4 ) + line
137
+ lines [i + 1 ] = strings .Repeat (" " , len (multilineKey )+ 4 ) + line
133
138
}
134
139
}
135
140
multilineVal = strings .Join (lines , "\n " )
136
141
137
- multilineKey = c (w , color .FgBlue ).Sprintf (`"%v"` , multilineKey )
138
- ents += fmt .Sprintf ("\n %v: %v" , multilineKey , multilineVal )
142
+ multilineKey = render (termW , multiLineKeyStyle , multilineKey )
143
+ buf .WriteString ("\n " )
144
+ buf .WriteString (multilineKey )
145
+ buf .WriteString ("= " )
146
+ buf .WriteString (multilineVal )
139
147
}
140
-
141
- return ents
142
148
}
143
149
144
- func levelColor (level slog.Level ) color.Attribute {
150
+ var (
151
+ levelDebugStyle = renderer .NewStyle ().Foreground (lipgloss .Color ("#ffffff" ))
152
+ levelInfoStyle = renderer .NewStyle ().Foreground (lipgloss .Color ("#0091FF" ))
153
+ levelWarnStyle = renderer .NewStyle ().Foreground (lipgloss .Color ("#FFCF0D" ))
154
+ levelErrorStyle = renderer .NewStyle ().Foreground (lipgloss .Color ("#FF5A0D" ))
155
+ )
156
+
157
+ func levelStyle (level slog.Level ) lipgloss.Style {
145
158
switch level {
146
159
case slog .LevelDebug :
147
- return color . Reset
160
+ return levelDebugStyle
148
161
case slog .LevelInfo :
149
- return color . FgBlue
162
+ return levelInfoStyle
150
163
case slog .LevelWarn :
151
- return color . FgYellow
152
- case slog .LevelError :
153
- return color . FgRed
164
+ return levelWarnStyle
165
+ case slog .LevelError , slog . LevelFatal , slog . LevelCritical :
166
+ return levelErrorStyle
154
167
default :
155
- return color . FgHiRed
168
+ panic ( "unknown level" )
156
169
}
157
170
}
158
171
@@ -196,64 +209,3 @@ func quoteKey(key string) string {
196
209
// Replace spaces in the map keys with underscores.
197
210
return strings .ReplaceAll (key , " " , "_" )
198
211
}
199
-
200
- var mainPackagePath string
201
- var mainModulePath string
202
-
203
- func init () {
204
- // Unfortunately does not work for tests yet :(
205
- // See https://github.com/golang/go/issues/33976
206
- bi , ok := debug .ReadBuildInfo ()
207
- if ! ok {
208
- return
209
- }
210
- mainPackagePath = bi .Path
211
- mainModulePath = bi .Main .Path
212
- }
213
-
214
- // humanPathAndFunc takes the absolute path to a file and an absolute module path to a
215
- // function in that file and returns the module path to the file. It also returns
216
- // the path to the function stripped of its module prefix.
217
- //
218
- // If the file is in the main Go module then its path is returned
219
- // relative to the main Go module's root.
220
- //
221
- // fn is from https://pkg.go.dev/runtime#Func.Name
222
- func humanPathAndFunc (filename , fn string ) (hpath , hfn string ) {
223
- // pkgDir is the dir of the pkg.
224
- // e.g. cdr.dev/slog/internal
225
- // base is the package name and the function name separated by a period.
226
- // e.g. entryhuman.humanPathAndFunc
227
- // There can be multiple periods when methods of types are involved.
228
- pkgDir , base := filepath .Split (fn )
229
- s := strings .Split (base , "." )
230
- pkg := s [0 ]
231
- hfn = strings .Join (s [1 :], "." )
232
-
233
- if pkg == "main" {
234
- // This happens with go build main.go
235
- if mainPackagePath == "command-line-arguments" {
236
- // Without a real mainPath, we can't find the path to the file
237
- // relative to the module. So we just return the base.
238
- return filepath .Base (filename ), hfn
239
- }
240
- // Go doesn't return the full main path in runtime.Func.Name()
241
- // It just returns the path "main"
242
- // Only runtime.ReadBuildInfo returns it so we have to check and replace.
243
- pkgDir = mainPackagePath
244
- // pkg main isn't reflected on the disk so we should not add it
245
- // into the pkgpath.
246
- pkg = ""
247
- }
248
-
249
- hpath = filepath .Join (pkgDir , pkg , filepath .Base (filename ))
250
-
251
- if mainModulePath != "" {
252
- relhpath , err := filepath .Rel (mainModulePath , hpath )
253
- if err == nil {
254
- hpath = "./" + relhpath
255
- }
256
- }
257
-
258
- return hpath , hfn
259
- }
0 commit comments