-
Notifications
You must be signed in to change notification settings - Fork 0
/
log.go
392 lines (331 loc) · 10.5 KB
/
log.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
// Package log provides a foundation for convenient, structured logging, centered around key-value blocks.
//
// The preferred usage pattern, for ultimate logging comfortableness, is to explicitly import this package using the "." notation, and replacing the DefaultLogger if needed.
// In case of identifier conflicts or special setups, you will likely want to at least alias the Log and Err methods of a Logger, as well as the Entry and Entries types.
//
// If you have static values that are reused throughout your code, consider preformatting them using a Formatter.
//
// Use the Node type to create log pipelines that can still be invoked through a single method call.
//
// Consider adding an unnamed Node pointer member to your central struct types. Don't forget to initialize it with MakeNode.
//
// type someType struct {
// ...
// *Node
// }
//
// func (x someType) someMethod() {
// ...
// x.Err(Warning, "oh no", someError)
// ...
// }
//
// Need your types to self log dynamic data? Have them implement EntriesGiver and pass them to the Node construction.
//
// x := someType{}
// x.Node = NewNode(DefaultLogger, staticStuff, x)
// ...
// x.Log(Emergency, "there's a handsome person in front of the screen")
package log
import (
"fmt"
"io"
"os"
"github.com/blitz-frost/log/logger"
)
// predefined log levels
const (
Default = iota // no assigned level
Debug // debug or trace information
Info // routine information
Notice // normal but significant events
Warning // might cause problems
Error // likely to cause problems
Critical // severe problems or brief outage
Alert // needs immediate action
Emergency // one or more systems are down
)
// The Logger used by the default package functions (Log, Err, Close).
// Can be replaced, in which case the old one should be closed first.
//
// Initialized to a LineLogger to stdout, which will not be closed when the Logger is closed.
var DefaultLogger Logger = LineLoggerMake(os.Stdout, func() {})
type Entry = logger.Entry
type Entries = logger.Entries
type EntriesGiver = logger.EntriesGiver
// ErrorLogger is a Logger extension that adds error logging convenience.
type ErrorLogger struct {
Logger
}
func (x ErrorLogger) Err(lvl int, msg string, err error, e ...EntriesGiver) {
LogError(x, lvl, msg, err, e...)
}
// A Preformatter preprocesses EntriesGivers to a type optimized for a particular Logger.
type Preformatter interface {
Preformat(EntriesGiver) EntriesGiver
}
// A Logger handles data formatting and transfer to a log destination (stdout, file, remote service, etc.).
// A log consists of multiple key-value pairs (a block). Implementations should support recursive block formatting (the value of a key may be a subblock).
//
// Logging is an ubiquitous action, and often the only way errors are handled, therefore a Logger should be error resilient itself and provide best effort functionality.
// Implementations should panic in case of fatal internal errors. For less severe errors, an "OnError" method could be provided.
//
// This interface is meant to be concise and generalistic. A fair degree of optimization is achievable through the use of prefered EntriesGiver implementations.
//
// Implementations should treat Entry collection synchronously, while formatting and backend transmission can/should be performed asynchronously.
// Conversely, EntriesGivers should return values that are immutable or stable.
type Logger interface {
Log(int, string, ...EntriesGiver) // should not modify mutable return values, such as Entry slices or mutable Entry values
}
// A Node facilitates logging flow by inserting predefined Entries as well as collecting from predefined EntriesGivers.
type Node struct {
dst Logger
src []EntriesGiver
}
// NodeMake creates a new usable Node using dst as the actual Logger implementation.
//
// static should return a set of immutable Entries, or at least guaranteed to not change throughout the lifespan of the Node.
// If dst is a Formatter, static will be passed through it on creation.
// May be nil, in which case it is ignored.
//
// src is an optional list of EntriesGivers that will be drawn from for each log.
//
// New logs will contain: (possible preformated) static + src element Entries + particular log data.
func NodeMake(dst Logger, static EntriesGiver, src ...EntriesGiver) Node {
var givers []EntriesGiver
if static != nil {
if p, ok := dst.(Preformatter); ok {
static = p.Preformat(static)
}
givers = append(givers, static)
}
givers = append(givers, src...)
return Node{
dst: dst,
src: givers,
}
}
func (x Node) Err(lvl int, msg string, err error, e ...EntriesGiver) {
LogError(x, lvl, msg, err, e...)
}
func (x Node) Log(lvl int, msg string, e ...EntriesGiver) {
givers := make([]EntriesGiver, len(x.src)+len(e))
copy(givers, x.src)
copy(givers[len(x.src):], e)
x.dst.Log(lvl, msg, givers...)
}
// A LineLogger writes logs to an io.Writer using the following format:
//
// LEVEL msg
// key0 - value0
// key1 - value1
// key2
// subkey0 - subvalue0
// subkey1 - subvalue1
//
// Its purpose is to provide human readable logs to stdout or local files.
type LineLogger struct {
logger.T[[]byte]
}
// LineLoggerMake returns a usable LineLogger.
// onClose may be nil, in which case it will default to closing the Writer, if it is also a io.Closer.
func LineLoggerMake(dst io.Writer, onClose func()) LineLogger {
return LineLogger{logger.Make[[]byte](lineCore{
w: dst,
onClose: onClose,
})}
}
func (x LineLogger) Preformat(e EntriesGiver) EntriesGiver {
return lineEntriesMake(e)
}
// errorBlock is an error type that may contain optional entries for logging.
// For calling efficiency, is a single Entry slice that starts with {"msg", [string]}.
// It may wrap another error, which will be appended as a final {"err", [error]} element.
type errorBlock []Entry
func (x errorBlock) Entries() Entries {
return Entries(x)
}
func (x errorBlock) Error() string {
return x[0].Value.(string)
}
func (x errorBlock) Unwrap() error {
v := x[len(x)-1].Value
if err, ok := v.(error); ok {
return err
}
return nil
}
// lineBuffer is the prefered formated block used by LineLogger.
type lineBuffer struct {
data []byte // preftormated lines
ends []int // line end indices; needed when working with preformated subblocks to insert additional spacing
space []byte // used to insert line spacing
}
// newLineBuffer allocates a lineBuffer with sufficient space for most uses
func newLineBuffer() *lineBuffer {
return &lineBuffer{
data: make([]byte, 0, 1024),
ends: make([]int, 0, 16),
space: []byte(" ")[:0],
}
}
func (x *lineBuffer) append(e EntriesGiver) {
if pre, ok := e.(lineEntries); ok {
// copy preformatted string, inserting appropriate spacing
start := 0
for _, end := range pre.buf.ends {
x.data = append(x.data, x.space...)
x.data = append(x.data, pre.buf.data[start:end]...)
x.ends = append(x.ends, len(x.data))
start = end
}
return
}
for _, entry := range e.Entries() {
x.appendEntry(entry)
}
}
func (x *lineBuffer) appendEntry(e Entry) {
x.data = append(x.data, x.space...)
x.data = append(x.data, e.Key...)
switch sub := e.Value.(type) {
case EntriesGiver:
x.endLine()
x.space = append(x.space, " "...)
x.append(sub)
x.space = x.space[:len(x.space)-2]
default:
// use default value formatting
x.data = append(x.data, " - "...)
x.data = fmt.Append(x.data, e.Value)
x.endLine()
}
}
func (x *lineBuffer) endLine() {
x.data = append(x.data, '\n')
x.ends = append(x.ends, len(x.data))
}
// reset clears all data while retaining allocated memory, making reuse more efficient than creating a new value
func (x *lineBuffer) reset() {
x.data = x.data[:0]
x.ends = x.ends[:0]
x.space = x.space[:0]
}
type lineCore struct {
w io.Writer
onClose func()
}
func (x lineCore) Close() {
if x.onClose != nil {
x.onClose()
return
}
if c, ok := x.w.(io.Closer); ok {
if err := c.Close(); err != nil {
panic(err)
}
}
}
func (x lineCore) Format(data logger.Data) []byte {
buf := newLineBuffer()
buf.data = append(buf.data, LevelString(data.Level)...)
buf.data = append(buf.data, " "...)
buf.data = append(buf.data, data.Message...)
buf.data = append(buf.data, '\n')
for _, elem := range data.Entries {
buf.append(elem)
}
buf.data = append(buf.data, '\n')
return buf.data
}
func (x lineCore) Write(b []byte) {
if _, err := x.w.Write(b); err != nil {
panic(err)
}
}
// lineEntries is the optimized preformated Entries for LinePrinter.
type lineEntries struct {
src []Entry
buf lineBuffer
}
func lineEntriesMake(src EntriesGiver) lineEntries {
if same, ok := src.(lineEntries); ok {
return same
}
buf := lineBuffer{}
entries := src.Entries()
for _, entry := range entries {
buf.appendEntry(entry)
}
return lineEntries{
src: entries,
buf: buf,
}
}
func (x lineEntries) Entries() Entries {
return x.src
}
// Close closes the DefaultLogger if it is a Closer.
func Close() {
if c, ok := DefaultLogger.(logger.Closer); ok {
c.Close()
}
}
// Err logs an error value using the DefaultLogger.
func Err(lvl int, msg string, err error, e ...EntriesGiver) {
LogError(DefaultLogger, lvl, msg, err, e...)
}
// Predefined level string forms (the constant identifier in all uppercase)
func LevelString(lvl int) string {
switch lvl {
case Default:
return "DEFAULT"
case Debug:
return "DEBUG"
case Info:
return "INFO"
case Notice:
return "NOTICE"
case Warning:
return "WARNING"
case Error:
return "ERROR"
case Critical:
return "CRITICAL"
case Alert:
return "ALERT"
case Emergency:
return "EMERGENCY"
}
return ""
}
// Log calls the DefaultLogger.
func Log(lvl int, msg string, e ...EntriesGiver) {
DefaultLogger.Log(lvl, msg, e...)
}
// LogError is a convenience function to handle errors of arbitrary type.
// Typically used to create "Err" methods.
func LogError(x Logger, lvl int, msg string, err error, e ...EntriesGiver) {
e = append(e, Entry{"err", err})
x.Log(lvl, msg, e...)
}
// ErrorMake creates a new error value that implements Entries and may contain additional logging information.
// If err is non-nil, the new error will wrap it.
func ErrorMake(msg string, err error, e ...EntriesGiver) error {
o := errorBlock{Entry{"msg", msg}}
for _, elem := range e {
o = append(o, elem.Entries()...)
}
if err != nil {
o = append(o, Entry{"err", err})
}
return o
}
// Preformat uses the DefaultLogger if it is a Preformatter.
// Otherwise returns the input unchanged.
func Preformat(e EntriesGiver) EntriesGiver {
if p, ok := DefaultLogger.(Preformatter); ok {
return p.Preformat(e)
}
return e
}