1
+// Copyright 2015 Matthew Holt and The Caddy Authors
2
+//
3
+// Licensed under the Apache License, Version 2.0 (the "License");
4
+// you may not use this file except in compliance with the License.
5
+// You may obtain a copy of the License at
6
+//
7
+// http://www.apache.org/licenses/LICENSE-2.0
8
+//
9
+// Unless required by applicable law or agreed to in writing, software
10
+// distributed under the License is distributed on an "AS IS" BASIS,
11
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+// See the License for the specific language governing permissions and
13
+// limitations under the License.
14
+
15
+package caddy
16
+
17
+import (
18
+ "encoding/json"
19
+ "fmt"
20
+ "io"
21
+ "io/ioutil"
22
+ "log"
23
+ "os"
24
+ "strings"
25
+ "sync"
26
+ "time"
27
+
28
+ "go.uber.org/zap"
29
+ "go.uber.org/zap/zapcore"
30
+)
31
+
32
+func init() {
33
+ RegisterModule(StdoutWriter{})
34
+ RegisterModule(StderrWriter{})
35
+ RegisterModule(DiscardWriter{})
36
+}
37
+
38
+// Logging facilitates logging within Caddy.
39
+type Logging struct {
40
+ Sink *StandardLibLog `json:"sink,omitempty"`
41
+ Logs map[string]*CustomLog `json:"logs,omitempty"`
42
+
43
+ // a list of all keys for open writers; all writers
44
+ // that are opened to provision this logging config
45
+ // must have their keys added to this list so they
46
+ // can be closed when cleaning up
47
+ writerKeys []string
48
+}
49
+
50
+// openLogs sets up the config and opens all the configured writers.
51
+// It closes its logs when ctx is cancelled, so it should clean up
52
+// after itself.
53
+func (logging *Logging) openLogs(ctx Context) error {
54
+ // make sure to deallocate resources when context is done
55
+ ctx.OnCancel(func() {
56
+ err := logging.closeLogs()
57
+ if err != nil {
58
+ Log().Error("closing logs", zap.Error(err))
59
+ }
60
+ })
61
+
62
+ // set up the "sink" log first (std lib's default global logger)
63
+ if logging.Sink != nil {
64
+ err := logging.Sink.provision(ctx, logging)
65
+ if err != nil {
66
+ return fmt.Errorf("setting up sink log: %v", err)
67
+ }
68
+ }
69
+
70
+ // as a special case, set up the default structured Caddy log next
71
+ if err := logging.setupNewDefault(ctx); err != nil {
72
+ return err
73
+ }
74
+
75
+ // then set up any other custom logs
76
+ for name, l := range logging.Logs {
77
+ // the default log is already set up
78
+ if name == "default" {
79
+ continue
80
+ }
81
+
82
+ err := l.provision(ctx, logging)
83
+ if err != nil {
84
+ return fmt.Errorf("setting up custom log '%s': %v", name, err)
85
+ }
86
+
87
+ // Any other logs that use the discard writer can be deleted
88
+ // entirely. This avoids encoding and processing of each
89
+ // log entry that would just be thrown away anyway. Notably,
90
+ // we do not reach this point for the default log, which MUST
91
+ // exist, otherwise core log emissions would panic because
92
+ // they use the Log() function directly which expects a non-nil
93
+ // logger. Even if we keep logs with a discard writer, they
94
+ // have a nop core, and keeping them at all seems unnecessary.
95
+ if _, ok := l.writerOpener.(*DiscardWriter); ok {
96
+ delete(logging.Logs, name)
97
+ continue
98
+ }
99
+ }
100
+
101
+ return nil
102
+}
103
+
104
+func (logging *Logging) setupNewDefault(ctx Context) error {
105
+ if logging.Logs == nil {
106
+ logging.Logs = make(map[string]*CustomLog)
107
+ }
108
+
109
+ // extract the user-defined default log, if any
110
+ newDefault := new(defaultCustomLog)
111
+ if userDefault, ok := logging.Logs["default"]; ok {
112
+ newDefault.CustomLog = userDefault
113
+ } else {
114
+ // if none, make one with our own default settings
115
+ var err error
116
+ newDefault, err = newDefaultProductionLog()
117
+ if err != nil {
118
+ return fmt.Errorf("setting up default Caddy log: %v", err)
119
+ }
120
+ logging.Logs["default"] = newDefault.CustomLog
121
+ }
122
+
123
+ // set up this new log
124
+ err := newDefault.CustomLog.provision(ctx, logging)
125
+ if err != nil {
126
+ return fmt.Errorf("setting up default log: %v", err)
127
+ }
128
+ newDefault.logger = zap.New(newDefault.CustomLog.core)
129
+
130
+ // redirect the default caddy logs
131
+ defaultLoggerMu.Lock()
132
+ oldDefault := defaultLogger
133
+ defaultLogger = newDefault
134
+ defaultLoggerMu.Unlock()
135
+
136
+ // if the new writer is different, indicate it in the logs for convenience
137
+ var newDefaultLogWriterKey, currentDefaultLogWriterKey string
138
+ var newDefaultLogWriterStr, currentDefaultLogWriterStr string
139
+ if newDefault.writerOpener != nil {
140
+ newDefaultLogWriterKey = newDefault.writerOpener.WriterKey()
141
+ newDefaultLogWriterStr = newDefault.writerOpener.String()
142
+ }
143
+ if oldDefault.writerOpener != nil {
144
+ currentDefaultLogWriterKey = oldDefault.writerOpener.WriterKey()
145
+ currentDefaultLogWriterStr = oldDefault.writerOpener.String()
146
+ }
147
+ if newDefaultLogWriterKey != currentDefaultLogWriterKey {
148
+ oldDefault.logger.Info("redirected default logger",
149
+ zap.String("from", currentDefaultLogWriterStr),
150
+ zap.String("to", newDefaultLogWriterStr),
151
+ )
152
+ }
153
+
154
+ return nil
155
+}
156
+
157
+// closeLogs cleans up resources allocated during openLogs.
158
+// A successful call to openLogs calls this automatically
159
+// when the context is cancelled.
160
+func (logging *Logging) closeLogs() error {
161
+ for _, key := range logging.writerKeys {
162
+ _, err := writers.Delete(key)
163
+ if err != nil {
164
+ log.Printf("[ERROR] Closing log writer %v: %v", key, err)
165
+ }
166
+ }
167
+ return nil
168
+}
169
+
170
+// Logger returns a logger that is ready for the module to use.
171
+func (logging *Logging) Logger(mod Module) *zap.Logger {
172
+ modName := mod.CaddyModule().Name
173
+ var cores []zapcore.Core
174
+
175
+ if logging != nil {
176
+ for _, l := range logging.Logs {
177
+ if l.matchesModule(modName) {
178
+ if len(l.Include) == 0 && len(l.Exclude) == 0 {
179
+ cores = append(cores, l.core)
180
+ continue
181
+ }
182
+ cores = append(cores, &filteringCore{Core: l.core, cl: l})
183
+ }
184
+ }
185
+ }
186
+
187
+ multiCore := zapcore.NewTee(cores...)
188
+
189
+ return zap.New(multiCore).Named(modName)
190
+}
191
+
192
+// openWriter opens a writer using opener, and returns true if
193
+// the writer is new, or false if the writer already exists.
194
+func (logging *Logging) openWriter(opener WriterOpener) (io.WriteCloser, bool, error) {
195
+ key := opener.WriterKey()
196
+ writer, loaded, err := writers.LoadOrNew(key, func() (Destructor, error) {
197
+ w, err := opener.OpenWriter()
198
+ return writerDestructor{w}, err
199
+ })
200
+ if err == nil {
201
+ logging.writerKeys = append(logging.writerKeys, key)
202
+ }
203
+ return writer.(io.WriteCloser), !loaded, err
204
+}
205
+
206
+// WriterOpener is a module that can open a log writer.
207
+// It can return a human-readable string representation
208
+// of itself so that operators can understand where
209
+// the logs are going.
210
+type WriterOpener interface {
211
+ fmt.Stringer
212
+
213
+ // WriterKey is a string that uniquely identifies this
214
+ // writer configuration. It is not shown to humans.
215
+ WriterKey() string
216
+
217
+ // OpenWriter opens a log for writing. The writer
218
+ // should be safe for concurrent use but need not
219
+ // be synchronous.
220
+ OpenWriter() (io.WriteCloser, error)
221
+}
222
+
223
+type writerDestructor struct {
224
+ io.WriteCloser
225
+}
226
+
227
+func (wdest writerDestructor) Destruct() error {
228
+ return wdest.Close()
229
+}
230
+
231
+// StandardLibLog configures the default Go standard library
232
+// global logger in the log package. This is necessary because
233
+// module dependencies which are not built specifically for
234
+// Caddy will use the standard logger.
235
+type StandardLibLog struct {
236
+ WriterRaw json.RawMessage `json:"writer,omitempty"`
237
+
238
+ writer io.WriteCloser
239
+}
240
+
241
+func (sll *StandardLibLog) provision(ctx Context, logging *Logging) error {
242
+ if sll.WriterRaw != nil {
243
+ val, err := ctx.LoadModuleInline("output", "caddy.logging.writers", sll.WriterRaw)
244
+ if err != nil {
245
+ return fmt.Errorf("loading sink log writer module: %v", err)
246
+ }
247
+ wo := val.(WriterOpener)
248
+ sll.WriterRaw = nil // allow GC to deallocate
249
+
250
+ var isNew bool
251
+ sll.writer, isNew, err = logging.openWriter(wo)
252
+ if err != nil {
253
+ return fmt.Errorf("opening sink log writer %#v: %v", val, err)
254
+ }
255
+
256
+ if isNew {
257
+ log.Printf("[INFO] Redirecting sink to: %s", wo)
258
+ log.SetOutput(sll.writer)
259
+ log.Printf("[INFO] Redirected sink to here (%s)", wo)
260
+ }
261
+ }
262
+
263
+ return nil
264
+}
265
+
266
+// CustomLog represents a custom logger configuration.
267
+type CustomLog struct {
268
+ WriterRaw json.RawMessage `json:"writer,omitempty"`
269
+ EncoderRaw json.RawMessage `json:"encoder,omitempty"`
270
+ Level string `json:"level,omitempty"`
271
+ Sampling *LogSampling `json:"sampling,omitempty"`
272
+ Include []string `json:"include,omitempty"`
273
+ Exclude []string `json:"exclude,omitempty"`
274
+
275
+ writerOpener WriterOpener
276
+ writer io.WriteCloser
277
+ encoder zapcore.Encoder
278
+ levelEnabler zapcore.LevelEnabler
279
+ core zapcore.Core
280
+}
281
+
282
+func (cl *CustomLog) provision(ctx Context, logging *Logging) error {
283
+ // set up the log level
284
+ switch cl.Level {
285
+ case "debug":
286
+ cl.levelEnabler = zapcore.DebugLevel
287
+ case "", "info":
288
+ cl.levelEnabler = zapcore.InfoLevel
289
+ case "warn":
290
+ cl.levelEnabler = zapcore.WarnLevel
291
+ case "error":
292
+ cl.levelEnabler = zapcore.ErrorLevel
293
+ case "panic":
294
+ cl.levelEnabler = zapcore.PanicLevel
295
+ case "fatal":
296
+ cl.levelEnabler = zapcore.FatalLevel
297
+ default:
298
+ return fmt.Errorf("unrecognized log level: %s", cl.Level)
299
+ }
300
+
301
+ // If both Include and Exclude lists are populated, then each item must
302
+ // be a superspace or subspace of an item in the other list, because
303
+ // populating both lists means that any given item is either a rule
304
+ // or an exception to another rule. But if the item is not a super-
305
+ // or sub-space of any item in the other list, it is neither a rule
306
+ // nor an exception, and is a contradiction. Ensure, too, that the
307
+ // sets do not intersect, which is also a contradiction.
308
+ if len(cl.Include) > 0 && len(cl.Exclude) > 0 {
309
+ // prevent intersections
310
+ for _, allow := range cl.Include {
311
+ for _, deny := range cl.Exclude {
312
+ if allow == deny {
313
+ return fmt.Errorf("include and exclude must not intersect, but found %s in both lists", allow)
314
+ }
315
+ }
316
+ }
317
+
318
+ // ensure namespaces are nested
319
+ outer:
320
+ for _, allow := range cl.Include {
321
+ for _, deny := range cl.Exclude {
322
+ if strings.HasPrefix(allow+".", deny+".") ||
323
+ strings.HasPrefix(deny+".", allow+".") {
324
+ continue outer
325
+ }
326
+ }
327
+ return fmt.Errorf("when both include and exclude are populated, each element must be a superspace or subspace of one in the other list; check '%s' in include", allow)
328
+ }
329
+ }
330
+
331
+ if cl.EncoderRaw != nil {
332
+ val, err := ctx.LoadModuleInline("format", "caddy.logging.encoders", cl.EncoderRaw)
333
+ if err != nil {
334
+ return fmt.Errorf("loading log encoder module: %v", err)
335
+ }
336
+ cl.EncoderRaw = nil // allow GC to deallocate
337
+ cl.encoder = val.(zapcore.Encoder)
338
+ }
339
+ if cl.encoder == nil {
340
+ cl.encoder = zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig())
341
+ }
342
+
343
+ if cl.WriterRaw != nil {
344
+ val, err := ctx.LoadModuleInline("output", "caddy.logging.writers", cl.WriterRaw)
345
+ if err != nil {
346
+ return fmt.Errorf("loading log writer module: %v", err)
347
+ }
348
+ cl.WriterRaw = nil // allow GC to deallocate
349
+ cl.writerOpener = val.(WriterOpener)
350
+ }
351
+ if cl.writerOpener == nil {
352
+ cl.writerOpener = StderrWriter{}
353
+ }
354
+ var err error
355
+ cl.writer, _, err = logging.openWriter(cl.writerOpener)
356
+ if err != nil {
357
+ return fmt.Errorf("opening log writer using %#v: %v", cl.writerOpener, err)
358
+ }
359
+
360
+ cl.buildCore()
361
+
362
+ return nil
363
+}
364
+
365
+func (cl *CustomLog) buildCore() {
366
+ // logs which only discard their output don't need
367
+ // to perform encoding or any other processing steps
368
+ // at all, so just shorcut to a nop core instead
369
+ if _, ok := cl.writerOpener.(*DiscardWriter); ok {
370
+ cl.core = zapcore.NewNopCore()
371
+ return
372
+ }
373
+ c := zapcore.NewCore(
374
+ cl.encoder,
375
+ zapcore.AddSync(cl.writer),
376
+ cl.levelEnabler,
377
+ )
378
+ if cl.Sampling != nil {
379
+ if cl.Sampling.Interval == 0 {
380
+ cl.Sampling.Interval = 1 * time.Second
381
+ }
382
+ if cl.Sampling.First == 0 {
383
+ cl.Sampling.First = 100
384
+ }
385
+ if cl.Sampling.Thereafter == 0 {
386
+ cl.Sampling.Thereafter = 100
387
+ }
388
+ c = zapcore.NewSampler(c, cl.Sampling.Interval,
389
+ cl.Sampling.First, cl.Sampling.Thereafter)
390
+ }
391
+ cl.core = c
392
+}
393
+
394
+func (cl *CustomLog) matchesModule(moduleName string) bool {
395
+ return cl.loggerAllowed(moduleName, true)
396
+}
397
+
398
+// loggerAllowed returns true if name is allowed to emit
399
+// to cl. isModule should be true if name is the name of
400
+// a module and you want to see if ANY of that module's
401
+// logs would be permitted.
402
+func (cl *CustomLog) loggerAllowed(name string, isModule bool) bool {
403
+ // accept all loggers by default
404
+ if len(cl.Include) == 0 && len(cl.Exclude) == 0 {
405
+ return true
406
+ }
407
+
408
+ // append a dot so that partial names don't match
409
+ // (i.e. we don't want "foo.b" to match "foo.bar"); we
410
+ // will also have to append a dot when we do HasPrefix
411
+ // below to compensate for when when namespaces are equal
412
+ if name != "" && name != "*" && name != "." {
413
+ name += "."
414
+ }
415
+
416
+ var longestAccept, longestReject int
417
+
418
+ if len(cl.Include) > 0 {
419
+ for _, namespace := range cl.Include {
420
+ var hasPrefix bool
421
+ if isModule {
422
+ hasPrefix = strings.HasPrefix(namespace+".", name)
423
+ } else {
424
+ hasPrefix = strings.HasPrefix(name, namespace+".")
425
+ }
426
+ if hasPrefix && len(namespace) > longestAccept {
427
+ longestAccept = len(namespace)
428
+ }
429
+ }
430
+ // the include list was populated, meaning that
431
+ // a match in this list is absolutely required
432
+ // if we are to accept the entry
433
+ if longestAccept == 0 {
434
+ return false
435
+ }
436
+ }
437
+
438
+ if len(cl.Exclude) > 0 {
439
+ for _, namespace := range cl.Exclude {
440
+ // * == all logs emitted by modules
441
+ // . == all logs emitted by core
442
+ if (namespace == "*" && name != ".") ||
443
+ (namespace == "." && name == ".") {
444
+ return false
445
+ }
446
+ if strings.HasPrefix(name, namespace+".") &&
447
+ len(namespace) > longestReject {
448
+ longestReject = len(namespace)
449
+ }
450
+ }
451
+ // the reject list is populated, so we have to
452
+ // reject this entry if its match is better
453
+ // than the best from the accept list
454
+ if longestReject > longestAccept {
455
+ return false
456
+ }
457
+ }
458
+
459
+ return (longestAccept > longestReject) ||
460
+ (len(cl.Include) == 0 && longestReject == 0)
461
+}
462
+
463
+// filteringCore filters log entries based on logger name,
464
+// according to the rules of a CustomLog.
465
+type filteringCore struct {
466
+ zapcore.Core
467
+ cl *CustomLog
468
+}
469
+
470
+// With properly wraps With.
471
+func (fc *filteringCore) With(fields []zapcore.Field) zapcore.Core {
472
+ return &filteringCore{
473
+ Core: fc.Core.With(fields),
474
+ cl: fc.cl,
475
+ }
476
+}
477
+
478
+// Check only allows the log entry if its logger name
479
+// is allowed from the include/exclude rules of fc.cl.
480
+func (fc *filteringCore) Check(e zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry {
481
+ if fc.cl.loggerAllowed(e.LoggerName, false) {
482
+ return fc.Core.Check(e, ce)
483
+ }
484
+ return ce
485
+}
486
+
487
+// LogSampling configures log entry sampling.
488
+type LogSampling struct {
489
+ Interval time.Duration `json:"interval,omitempty"`
490
+ First int `json:"first,omitempty"`
491
+ Thereafter int `json:"thereafter,omitempty"`
492
+}
493
+
494
+type (
495
+ // StdoutWriter can write logs to stdout.
496
+ StdoutWriter struct{}
497
+
498
+ // StderrWriter can write logs to stdout.
499
+ StderrWriter struct{}
500
+
501
+ // DiscardWriter discards all writes.
502
+ DiscardWriter struct{}
503
+)
504
+
505
+// CaddyModule returns the Caddy module information.
506
+func (StdoutWriter) CaddyModule() ModuleInfo {
507
+ return ModuleInfo{
508
+ Name: "caddy.logging.writers.stdout",
509
+ New: func() Module { return new(StdoutWriter) },
510
+ }
511
+}
512
+
513
+// CaddyModule returns the Caddy module information.
514
+func (StderrWriter) CaddyModule() ModuleInfo {
515
+ return ModuleInfo{
516
+ Name: "caddy.logging.writers.stderr",
517
+ New: func() Module { return new(StderrWriter) },
518
+ }
519
+}
520
+
521
+// CaddyModule returns the Caddy module information.
522
+func (DiscardWriter) CaddyModule() ModuleInfo {
523
+ return ModuleInfo{
524
+ Name: "caddy.logging.writers.discard",
525
+ New: func() Module { return new(DiscardWriter) },
526
+ }
527
+}
528
+
529
+func (StdoutWriter) String() string { return "stdout" }
530
+func (StderrWriter) String() string { return "stderr" }
531
+func (DiscardWriter) String() string { return "discard" }
532
+
533
+// WriterKey returns a unique key representing stdout.
534
+func (StdoutWriter) WriterKey() string { return "std:out" }
535
+
536
+// WriterKey returns a unique key representing stderr.
537
+func (StderrWriter) WriterKey() string { return "std:err" }
538
+
539
+// WriterKey returns a unique key representing discard.
540
+func (DiscardWriter) WriterKey() string { return "discard" }
541
+
542
+// OpenWriter returns os.Stdout that can't be closed.
543
+func (StdoutWriter) OpenWriter() (io.WriteCloser, error) {
544
+ return notClosable{os.Stdout}, nil
545
+}
546
+
547
+// OpenWriter returns os.Stderr that can't be closed.
548
+func (StderrWriter) OpenWriter() (io.WriteCloser, error) {
549
+ return notClosable{os.Stderr}, nil
550
+}
551
+
552
+// OpenWriter returns ioutil.Discard that can't be closed.
553
+func (DiscardWriter) OpenWriter() (io.WriteCloser, error) {
554
+ return notClosable{ioutil.Discard}, nil
555
+}
556
+
557
+// notClosable is an io.WriteCloser that can't be closed.
558
+type notClosable struct{ io.Writer }
559
+
560
+func (fc notClosable) Close() error { return nil }
561
+
562
+type defaultCustomLog struct {
563
+ *CustomLog
564
+ logger *zap.Logger
565
+}
566
+
567
+// newDefaultProductionLog configures a custom log that is
568
+// intended for use by default if no other log is specified
569
+// in a config. It writes to stderr, uses the console encoder,
570
+// and enables INFO-level logs and higher.
571
+func newDefaultProductionLog() (*defaultCustomLog, error) {
572
+ cl := new(CustomLog)
573
+ cl.writerOpener = StderrWriter{}
574
+ var err error
575
+ cl.writer, err = cl.writerOpener.OpenWriter()
576
+ if err != nil {
577
+ return nil, err
578
+ }
579
+ encCfg := zap.NewProductionEncoderConfig()
580
+ cl.encoder = zapcore.NewConsoleEncoder(encCfg)
581
+ cl.levelEnabler = zapcore.InfoLevel
582
+
583
+ cl.buildCore()
584
+
585
+ return &defaultCustomLog{
586
+ CustomLog: cl,
587
+ logger: zap.New(cl.core),
588
+ }, nil
589
+}
590
+
591
+// Log returns the current default logger.
592
+func Log() *zap.Logger {
593
+ defaultLoggerMu.RLock()
594
+ defer defaultLoggerMu.RUnlock()
595
+ return defaultLogger.logger
596
+}
597
+
598
+var (
599
+ defaultLogger, _ = newDefaultProductionLog()
600
+ defaultLoggerMu sync.RWMutex
601
+)
602
+
603
+var writers = NewUsagePool()
604
+
605
+// Interface guards
606
+var (
607
+ _ io.WriteCloser = (*notClosable)(nil)
608
+ _ WriterOpener = (*StdoutWriter)(nil)
609
+ _ WriterOpener = (*StderrWriter)(nil)
610
+)