This repository has been archived by the owner on Nov 5, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathlogger.go
238 lines (198 loc) · 6.84 KB
/
logger.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
package clog
import (
"context"
"os"
"sync"
"time"
"github.com/alcionai/clues"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// Yes, we just hijack zap for our logging needs here.
// This package isn't about writing a logger, it's about
// adding an opinionated shell around the zap logger.
var (
cloggerton *clogger
singleMu sync.Mutex
)
type clogger struct {
zsl *zap.SugaredLogger
set Settings
}
// ---------------------------------------------------------------------------
// constructors
// ---------------------------------------------------------------------------
func genLogger(set Settings) *zap.SugaredLogger {
// when testing, ensure debug logging matches the test.v setting
for _, arg := range os.Args {
if arg == `--test.v=true` {
set.Level = LevelDebug
}
}
var (
// this will be the backbone logger for the clogs
// TODO: would be nice to accept a variety of loggers here, and
// treat this all as a shim. Oh well, gotta start somewhere.
zlog *zap.Logger
zcfg zap.Config
// by default only add stacktraces to panics, else it gets too noisy.
zopts = []zap.Option{
zap.AddStacktrace(zapcore.PanicLevel),
zap.AddCallerSkip(2),
}
)
switch set.Format {
// JSON means each row should appear as a single json object.
case FormatToJSON:
zcfg = setLevel(zap.NewProductionConfig(), set.Level)
zcfg.OutputPaths = []string{set.File}
// by default we'll use the columnar non-json format, which uses tab
// separated values within each line, and may contain multiple json objs.
default:
zcfg = setLevel(zap.NewDevelopmentConfig(), set.Level)
zcfg.EncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout(time.StampMilli)
// when printing to stdout/stderr, colorize things!
if set.File == Stderr || set.File == Stdout {
zcfg.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
}
}
zcfg.OutputPaths = []string{set.File}
zlog, err := zcfg.Build(zopts...)
if err != nil {
zlog = zapcoreFallback(set)
}
// TODO: wrap the sugar logger to be a sugar... clogger...
return zlog.Sugar()
}
// set up a logger core to use as a fallback in case the config doesn't work.
// we shouldn't ever need this, but it's nice to know there's a fallback in
// case configuration gets buggery, because everyone still wants their logs.
func zapcoreFallback(set Settings) *zap.Logger {
levelFilter := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
switch set.Level {
case LevelInfo:
return lvl >= zapcore.InfoLevel
case LevelError:
return lvl >= zapcore.ErrorLevel
case LevelDisabled:
return false
default:
// default to debug
return true
}
})
// build out the zapcore fallback
var (
out = zapcore.Lock(os.Stderr)
consoleEncoder = zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())
core = zapcore.NewTee(zapcore.NewCore(consoleEncoder, out, levelFilter))
)
return zap.New(core)
}
// converts a given logLevel into the zapcore level enum.
func setLevel(cfg zap.Config, level logLevel) zap.Config {
switch level {
case LevelInfo:
cfg.Level = zap.NewAtomicLevelAt(zapcore.InfoLevel)
case LevelError:
cfg.Level = zap.NewAtomicLevelAt(zapcore.ErrorLevel)
case LevelDisabled:
cfg.Level = zap.NewAtomicLevelAt(zapcore.FatalLevel)
}
return cfg
}
// singleton is the constructor and getter in one. Since we manage a global
// singleton for each instance, we only ever want one alive at any given time.
func singleton(set Settings) *clogger {
singleMu.Lock()
defer singleMu.Unlock()
if cloggerton != nil {
return cloggerton
}
set = set.EnsureDefaults()
setCluesSecretsHash(set.SensitiveInfoHandling)
zsl := genLogger(set)
cloggerton = &clogger{
zsl: zsl,
set: set,
}
return cloggerton
}
// ------------------------------------------------------------------------------------------------
// context management
// ------------------------------------------------------------------------------------------------
type loggingKey string
const ctxKey loggingKey = "clog_logger"
// Init embeds a logger within the context for later retrieval.
// It is a preferred, but not necessary, initialization step.
// If you don't call this and you start logging, or you call
// Singleton(), then the package will initialize a logger instance
// with the default values. If you need to configure your logs,
// make sure to embed this first.
func Init(ctx context.Context, set Settings) context.Context {
clogged := singleton(set)
clogged.zsl.Debugw("seeding logger", "logger_settings", set)
return plantLoggerInCtx(ctx, clogged)
}
// PlantLogger allows users to embed their own zap.SugaredLogger within the context.
// It's good for inheriting a logger instance that was generated elsewhere, in case
// you have a downstream package that wants to clog the code with a different zsl.
func PlantLogger(ctx context.Context, seed *zap.SugaredLogger) context.Context {
return plantLoggerInCtx(ctx, &clogger{zsl: seed})
}
// plantLoggerInCtx allows users to embed their own zap.SugaredLogger within the
// context and with the given logger settings.
func plantLoggerInCtx(
ctx context.Context,
clogger *clogger,
) context.Context {
if clogger == nil {
return ctx
}
return context.WithValue(ctx, ctxKey, clogger)
}
// fromCtx pulls the clogger out of the context. If no logger exists in the
// ctx, it returns the global singleton.
func fromCtx(ctx context.Context) *clogger {
l := ctx.Value(ctxKey)
// if l is still nil, we need to grab the global singleton or construct a singleton.
if l == nil {
l = singleton(Settings{}.EnsureDefaults())
}
return l.(*clogger)
}
// Ctx retrieves the logger embedded in the context.
// It also extracts any clues from the ctx and adds all k:v pairs to that log instance.
func Ctx(ctx context.Context) *builder {
return newBuilder(ctx)
}
// CtxErr is a shorthand for clog.Ctx(ctx).Err(err)
func CtxErr(ctx context.Context, err error) *builder {
nb := newBuilder(ctx)
nb.err = err
return nb
}
// Singleton is a shorthand for .Ctx(context.Background()). IE: it'll use the singleton
// logger directly; building one if necessary. You should avoid this and use .Ctx or
// .CtxErr if possible. Likelihood is that you're somewhere deep in a func chain that
// doesn't accept a ctx, and you still want to add a quick log; maybe for debugging purposes.
//
// That's fine! Everything should work great.
//
// Unless you call this before initialization. Then it'll panic. We do want you to init
// the logger first, else you'll potentially lose these logs due different buffers.
func Singleton() *builder {
if cloggerton == nil {
panic(clues.New("clog singleton requires prior initialization"))
}
return &builder{
ctx: context.Background(),
zsl: cloggerton.zsl,
}
}
// Flush writes out all buffered logs.
// Probably good to do before shutting down whatever instance
// had initialized the singleton.
func Flush(ctx context.Context) {
_ = Ctx(ctx).zsl.Sync()
}