-
Notifications
You must be signed in to change notification settings - Fork 1
/
flash.go
353 lines (294 loc) · 7.87 KB
/
flash.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
// Package flash configures an opinionated zap logger.
package flash
import (
"fmt"
"net/url"
"os"
"strings"
"sync"
"github.com/mattn/go-isatty"
"github.com/prometheus/client_golang/prometheus"
zaplogfmt "github.com/sykesm/zap-logfmt"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
)
const (
lumberjackSinkURIPrefix = "lumberjack"
)
// EncoderType is a zap encoder.
type EncoderType int
// All supported encoder types.
const (
Console EncoderType = iota
JSON
LogFmt
)
// Logger is the flash logger which embeds a `zap.SugaredLogger`.
type Logger struct {
*zap.SugaredLogger
atom zap.AtomicLevel
m sync.Mutex
currentLevel zapcore.Level
disableStackTrace bool
}
// Option configures zap.Config.
type Option func(c *config)
// WithEncoder configures the zap encoder.
func WithEncoder(e EncoderType) Option {
return func(c *config) {
c.encoder = e
}
}
// WithColor enables color output.
func WithColor() Option {
return func(c *config) {
c.enableColor = true
}
}
// WithoutCaller stops annotating logs with the calling function's file
// name and line number.
func WithoutCaller() Option {
return func(c *config) {
c.disableCaller = true
}
}
// WithSinks changes the default zap `stderr` sink.
func WithSinks(sinks ...string) Option {
return func(c *config) {
c.sinks = sinks
}
}
// WithDebug enables or disables `DebugLevel`.
func WithDebug(debug bool) Option {
return func(c *config) {
c.isDebug = debug
}
}
// WithStacktrace completely enables automatic stacktrace capturing. Stacktraces
// are captured on `ErrorLevel` and above when in debug mode. When not in debug mode,
// only `FatalLevel` messages contain stacktraces.
func WithStacktrace() Option {
return func(c *config) {
c.disableStacktrace = false
}
}
// WithPrometheus registers a prometheus log message counter.
//
// The created metrics are of the form:
//
// <appName>_log_messages_total{level="info"} 4
//
// If appName is an empty string `flash` is used.
func WithPrometheus(appName string, registry prometheus.Registerer) Option {
return func(c *config) {
name := appName
if name == "" {
name = "flash"
}
counter := prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: fmt.Sprintf("%s_log_messages_total", name),
Help: "How many log messages created, partitioned by log level.",
},
[]string{"level"},
)
registry.MustRegister(counter)
c.hook = func(e zapcore.Entry) error {
counter.WithLabelValues(e.Level.String()).Inc()
return nil
}
}
}
// WithFile configures the logger to log output into a file.
func WithFile(cfg FileConfig) Option {
return func(c *config) {
c.fileConfig = &cfg
}
}
// WithoutTimestamps configures the logger to log without timestamps.
func WithoutTimestamps() Option {
return func(c *config) {
c.disableTimestamps = true
}
}
// FileConfig holds the configuration for logging into a file. The size is in Megabytes and
// MaxAge is in days. If compress is true the rotated files are compressed.
type FileConfig struct {
Path string
MaxSize int
MaxBackups int
MaxAge int
Compress bool
}
// New creates a new Logger. If no options are specified, stacktraces and color output are disabled and
// the confgured level is `InfoLevel`.
func New(opts ...Option) *Logger {
atom := zap.NewAtomicLevelAt(zap.InfoLevel)
cfg := config{
disableStacktrace: true,
encoder: Console,
}
// set encoder to json and disable color output when no terminal is detected
// and encoder is not LogFmt
if !isatty.IsTerminal(os.Stdout.Fd()) && cfg.encoder != LogFmt {
cfg.encoder = JSON
cfg.enableColor = false
}
for _, opt := range opts {
opt(&cfg)
}
if cfg.encoder != Console {
cfg.enableColor = false
}
zapConfig := genZapConfig(cfg)
zapConfig.Level = atom
var err error
l, err := zapConfig.Build()
if err != nil {
panic(fmt.Sprintf("could not create zap logger: %s", err))
}
stackTraceLevel := zap.FatalLevel
if cfg.isDebug {
atom.SetLevel(zap.DebugLevel)
stackTraceLevel = zap.ErrorLevel
}
// fix level for stack traces
if !cfg.disableStacktrace {
l = l.WithOptions(zap.AddStacktrace(stackTraceLevel))
}
if cfg.hook != nil {
l = l.WithOptions(zap.Hooks(cfg.hook))
}
defer func() {
_ = l.Sync()
}()
return &Logger{
SugaredLogger: l.Sugar(),
atom: atom,
disableStackTrace: cfg.disableStacktrace,
}
}
// SetDebug enables or disables `DebugLevel`.
func (l *Logger) SetDebug(d bool) {
level := zap.DebugLevel
stackTraceLevel := zap.ErrorLevel
if !d {
l.m.Lock()
level = l.currentLevel
l.m.Unlock()
stackTraceLevel = zap.FatalLevel
}
l.atom.SetLevel(level)
l.stackTrace(stackTraceLevel)
}
// Disable disables (nearly) all output. Only `FatalLevel` errors are logged.
func (l *Logger) Disable() {
l.m.Lock()
l.currentLevel = zapcore.FatalLevel
l.m.Unlock()
l.atom.SetLevel(zap.FatalLevel)
}
// SetLevel sets the chosen level. If stacktraces are enabled, it adjusts stacktrace levels accordingly.
func (l *Logger) SetLevel(level zapcore.Level) {
l.m.Lock()
oldLevel := l.currentLevel
l.currentLevel = level
l.m.Unlock()
l.atom.SetLevel(level)
if level == zap.DebugLevel {
l.stackTrace(zap.ErrorLevel)
return
}
if oldLevel == zap.DebugLevel && level != zap.DebugLevel {
l.stackTrace(zap.FatalLevel)
}
}
// Get returns the embedded zap.Logger
func (l *Logger) Get() *zap.SugaredLogger {
return l.SugaredLogger
}
func (l *Logger) stackTrace(lvl zapcore.Level) {
if l.disableStackTrace {
return
}
l.m.Lock()
l.SugaredLogger = l.Get().Desugar().WithOptions(zap.AddStacktrace(lvl)).Sugar()
l.m.Unlock()
}
type config struct {
enableColor bool
disableCaller bool
disableStacktrace bool
disableTimestamps bool
isDebug bool
hook func(zapcore.Entry) error
sinks []string
fileConfig *FileConfig
encoder EncoderType
}
func (cfg FileConfig) sinkURI() string {
return fmt.Sprintf("%s://localhost/%s", lumberjackSinkURIPrefix, cfg.Path)
}
func pathFromURI(u *url.URL) string {
return strings.Replace(u.Path, "/", "", 1)
}
type lumberjackSink struct {
*lumberjack.Logger
}
// Sync implements zap.Sink. The remaining methods are implemented
// by the embedded *lumberjack.Logger.
func (lumberjackSink) Sync() error { return nil }
func (c config) registerFileSink() error {
return zap.RegisterSink(lumberjackSinkURIPrefix, func(u *url.URL) (zap.Sink, error) {
return lumberjackSink{
Logger: &lumberjack.Logger{
Filename: pathFromURI(u),
MaxSize: c.fileConfig.MaxSize,
MaxAge: c.fileConfig.MaxAge,
MaxBackups: c.fileConfig.MaxBackups,
Compress: c.fileConfig.Compress,
},
}, nil
})
}
func genZapConfig(cfg config) zap.Config {
zapConfig := zap.NewProductionConfig()
zapConfig.DisableStacktrace = cfg.disableStacktrace
zapConfig.Sampling = nil
zapConfig.DisableCaller = cfg.disableCaller
zapConfig.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
zapConfig.EncoderConfig.EncodeDuration = zapcore.StringDurationEncoder
zapConfig.EncoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
switch cfg.encoder {
case Console:
zapConfig.Encoding = "console"
case JSON:
zapConfig.Encoding = "json"
case LogFmt:
zapConfig.Encoding = "logfmt"
_ = zap.RegisterEncoder("logfmt", func(cfg zapcore.EncoderConfig) (zapcore.Encoder, error) {
return zaplogfmt.NewEncoder(cfg), nil
})
}
// no colors when logging to file
if cfg.enableColor && cfg.fileConfig == nil {
zapConfig.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
}
if len(cfg.sinks) > 0 {
zapConfig.OutputPaths = cfg.sinks
}
if cfg.disableTimestamps {
zapConfig.EncoderConfig.TimeKey = ""
}
if cfg.fileConfig != nil {
if err := cfg.registerFileSink(); err != nil {
panic(err)
}
zapConfig.OutputPaths = []string{cfg.fileConfig.sinkURI()}
}
if cfg.disableTimestamps {
zapConfig.EncoderConfig.TimeKey = ""
}
return zapConfig
}