1
2
3 package entryhuman
4
5 import (
6 "bytes"
7 "encoding/json"
8 "fmt"
9 "io"
10 "os"
11 "path/filepath"
12 "runtime/debug"
13 "strconv"
14 "strings"
15 "time"
16
17 "github.com/fatih/color"
18 "go.opencensus.io/trace"
19 "golang.org/x/crypto/ssh/terminal"
20 "golang.org/x/xerrors"
21
22 "cdr.dev/slog"
23 )
24
25
26
27 func StripTimestamp(ent string) (time.Time, string, error) {
28 ts := ent[:len(TimeFormat)]
29 rest := ent[len(TimeFormat):]
30 et, err := time.Parse(TimeFormat, ts)
31 return et, rest, err
32 }
33
34
35 const TimeFormat = "2006-01-02 15:04:05.000"
36
37 func c(w io.Writer, attrs ...color.Attribute) *color.Color {
38 c := color.New(attrs...)
39 c.DisableColor()
40 if shouldColor(w) {
41 c.EnableColor()
42 }
43 return c
44 }
45
46
47
48
49
50
51
52
53 func Fmt(w io.Writer, ent slog.SinkEntry) string {
54 ents := c(w, color.Reset).Sprint("")
55 ts := ent.Time.Format(TimeFormat)
56 ents += ts + " "
57
58 level := "[" + ent.Level.String() + "]"
59 level = c(w, levelColor(ent.Level)).Sprint(level)
60 ents += fmt.Sprintf("%v\t", level)
61
62 if len(ent.LoggerNames) > 0 {
63 loggerName := "(" + quoteKey(strings.Join(ent.LoggerNames, ".")) + ")"
64 loggerName = c(w, color.FgMagenta).Sprint(loggerName)
65 ents += fmt.Sprintf("%v\t", loggerName)
66 }
67
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 var multilineKey string
74 var multilineVal string
75 msg := strings.TrimSpace(ent.Message)
76 if strings.Contains(msg, "\n") {
77 multilineKey = "msg"
78 multilineVal = msg
79 msg = "..."
80 }
81 msg = quote(msg)
82 ents += msg
83
84 if ent.SpanContext != (trace.SpanContext{}) {
85 ent.Fields = append(slog.M(
86 slog.F("trace", ent.SpanContext.TraceID),
87 slog.F("span", ent.SpanContext.SpanID),
88 ), ent.Fields...)
89 }
90
91 for i, f := range ent.Fields {
92 if multilineVal != "" {
93 break
94 }
95
96 var s string
97 switch v := f.Value.(type) {
98 case string:
99 s = v
100 case error, xerrors.Formatter:
101 s = fmt.Sprintf("%+v", v)
102 }
103 s = strings.TrimSpace(s)
104 if !strings.Contains(s, "\n") {
105 continue
106 }
107
108
109 ent.Fields = append(ent.Fields[:i], ent.Fields[i+1:]...)
110 multilineKey = f.Name
111 multilineVal = s
112 }
113
114 if len(ent.Fields) > 0 {
115
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)
121 }
122
123 if multilineVal != "" {
124 if msg != "..." {
125 ents += " ..."
126 }
127
128
129 lines := strings.Split(multilineVal, "\n")
130 for i, line := range lines[1:] {
131 if line != "" {
132 lines[i+1] = c(w, color.Reset).Sprint("") + strings.Repeat(" ", len(multilineKey)+4) + line
133 }
134 }
135 multilineVal = strings.Join(lines, "\n")
136
137 multilineKey = c(w, color.FgBlue).Sprintf(`"%v"`, multilineKey)
138 ents += fmt.Sprintf("\n%v: %v", multilineKey, multilineVal)
139 }
140
141 return ents
142 }
143
144 func levelColor(level slog.Level) color.Attribute {
145 switch level {
146 case slog.LevelDebug:
147 return color.Reset
148 case slog.LevelInfo:
149 return color.FgBlue
150 case slog.LevelWarn:
151 return color.FgYellow
152 case slog.LevelError:
153 return color.FgRed
154 default:
155 return color.FgHiRed
156 }
157 }
158
159 var forceColorWriter = io.Writer(&bytes.Buffer{})
160
161
162 func isTTY(w io.Writer) bool {
163 if w == forceColorWriter {
164 return true
165 }
166 f, ok := w.(interface {
167 Fd() uintptr
168 })
169 return ok && terminal.IsTerminal(int(f.Fd()))
170 }
171
172 func shouldColor(w io.Writer) bool {
173 return isTTY(w) || os.Getenv("FORCE_COLOR") != ""
174 }
175
176
177
178
179 func quote(key string) string {
180
181 if key == "" {
182 return `""`
183 }
184
185 quoted := strconv.Quote(key)
186
187
188
189 if quoted[1:len(quoted)-1] == key {
190 return key
191 }
192 return quoted
193 }
194
195 func quoteKey(key string) string {
196
197 return strings.ReplaceAll(key, " ", "_")
198 }
199
200 var mainPackagePath string
201 var mainModulePath string
202
203 func init() {
204
205
206 bi, ok := debug.ReadBuildInfo()
207 if !ok {
208 return
209 }
210 mainPackagePath = bi.Path
211 mainModulePath = bi.Main.Path
212 }
213
214
215
216
217
218
219
220
221
222 func humanPathAndFunc(filename, fn string) (hpath, hfn string) {
223
224
225
226
227
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
235 if mainPackagePath == "command-line-arguments" {
236
237
238 return filepath.Base(filename), hfn
239 }
240
241
242
243 pkgDir = mainPackagePath
244
245
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 }
260
View as plain text