Browse Source
* logging: Initial implementation * logging: More encoder formats, better defaults * logging: Fix repetition bug with FilterEncoder; add more presets * logging: DiscardWriter; delete or no-op logs that discard their output * logging: Add http.handlers.log module; enhance Replacer methods The Replacer interface has new methods to customize how to handle empty or unrecognized placeholders. Closes #2815. * logging: Overhaul HTTP logging, fix bugs, improve filtering, etc. * logging: General cleanup, begin transitioning to using new loggers * Fixes after merge conflictmaster
Matt Holt
5 years ago
committed by
GitHub
34 changed files with 2234 additions and 201 deletions
@ -0,0 +1,610 @@ |
|||||
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
|
//
|
||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
// you may not use this file except in compliance with the License.
|
||||
|
// You may obtain a copy of the License at
|
||||
|
//
|
||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
//
|
||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
// See the License for the specific language governing permissions and
|
||||
|
// limitations under the License.
|
||||
|
|
||||
|
package caddy |
||||
|
|
||||
|
import ( |
||||
|
"encoding/json" |
||||
|
"fmt" |
||||
|
"io" |
||||
|
"io/ioutil" |
||||
|
"log" |
||||
|
"os" |
||||
|
"strings" |
||||
|
"sync" |
||||
|
"time" |
||||
|
|
||||
|
"go.uber.org/zap" |
||||
|
"go.uber.org/zap/zapcore" |
||||
|
) |
||||
|
|
||||
|
func init() { |
||||
|
RegisterModule(StdoutWriter{}) |
||||
|
RegisterModule(StderrWriter{}) |
||||
|
RegisterModule(DiscardWriter{}) |
||||
|
} |
||||
|
|
||||
|
// Logging facilitates logging within Caddy.
|
||||
|
type Logging struct { |
||||
|
Sink *StandardLibLog `json:"sink,omitempty"` |
||||
|
Logs map[string]*CustomLog `json:"logs,omitempty"` |
||||
|
|
||||
|
// a list of all keys for open writers; all writers
|
||||
|
// that are opened to provision this logging config
|
||||
|
// must have their keys added to this list so they
|
||||
|
// can be closed when cleaning up
|
||||
|
writerKeys []string |
||||
|
} |
||||
|
|
||||
|
// openLogs sets up the config and opens all the configured writers.
|
||||
|
// It closes its logs when ctx is cancelled, so it should clean up
|
||||
|
// after itself.
|
||||
|
func (logging *Logging) openLogs(ctx Context) error { |
||||
|
// make sure to deallocate resources when context is done
|
||||
|
ctx.OnCancel(func() { |
||||
|
err := logging.closeLogs() |
||||
|
if err != nil { |
||||
|
Log().Error("closing logs", zap.Error(err)) |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
// set up the "sink" log first (std lib's default global logger)
|
||||
|
if logging.Sink != nil { |
||||
|
err := logging.Sink.provision(ctx, logging) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("setting up sink log: %v", err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// as a special case, set up the default structured Caddy log next
|
||||
|
if err := logging.setupNewDefault(ctx); err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
// then set up any other custom logs
|
||||
|
for name, l := range logging.Logs { |
||||
|
// the default log is already set up
|
||||
|
if name == "default" { |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
err := l.provision(ctx, logging) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("setting up custom log '%s': %v", name, err) |
||||
|
} |
||||
|
|
||||
|
// Any other logs that use the discard writer can be deleted
|
||||
|
// entirely. This avoids encoding and processing of each
|
||||
|
// log entry that would just be thrown away anyway. Notably,
|
||||
|
// we do not reach this point for the default log, which MUST
|
||||
|
// exist, otherwise core log emissions would panic because
|
||||
|
// they use the Log() function directly which expects a non-nil
|
||||
|
// logger. Even if we keep logs with a discard writer, they
|
||||
|
// have a nop core, and keeping them at all seems unnecessary.
|
||||
|
if _, ok := l.writerOpener.(*DiscardWriter); ok { |
||||
|
delete(logging.Logs, name) |
||||
|
continue |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (logging *Logging) setupNewDefault(ctx Context) error { |
||||
|
if logging.Logs == nil { |
||||
|
logging.Logs = make(map[string]*CustomLog) |
||||
|
} |
||||
|
|
||||
|
// extract the user-defined default log, if any
|
||||
|
newDefault := new(defaultCustomLog) |
||||
|
if userDefault, ok := logging.Logs["default"]; ok { |
||||
|
newDefault.CustomLog = userDefault |
||||
|
} else { |
||||
|
// if none, make one with our own default settings
|
||||
|
var err error |
||||
|
newDefault, err = newDefaultProductionLog() |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("setting up default Caddy log: %v", err) |
||||
|
} |
||||
|
logging.Logs["default"] = newDefault.CustomLog |
||||
|
} |
||||
|
|
||||
|
// set up this new log
|
||||
|
err := newDefault.CustomLog.provision(ctx, logging) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("setting up default log: %v", err) |
||||
|
} |
||||
|
newDefault.logger = zap.New(newDefault.CustomLog.core) |
||||
|
|
||||
|
// redirect the default caddy logs
|
||||
|
defaultLoggerMu.Lock() |
||||
|
oldDefault := defaultLogger |
||||
|
defaultLogger = newDefault |
||||
|
defaultLoggerMu.Unlock() |
||||
|
|
||||
|
// if the new writer is different, indicate it in the logs for convenience
|
||||
|
var newDefaultLogWriterKey, currentDefaultLogWriterKey string |
||||
|
var newDefaultLogWriterStr, currentDefaultLogWriterStr string |
||||
|
if newDefault.writerOpener != nil { |
||||
|
newDefaultLogWriterKey = newDefault.writerOpener.WriterKey() |
||||
|
newDefaultLogWriterStr = newDefault.writerOpener.String() |
||||
|
} |
||||
|
if oldDefault.writerOpener != nil { |
||||
|
currentDefaultLogWriterKey = oldDefault.writerOpener.WriterKey() |
||||
|
currentDefaultLogWriterStr = oldDefault.writerOpener.String() |
||||
|
} |
||||
|
if newDefaultLogWriterKey != currentDefaultLogWriterKey { |
||||
|
oldDefault.logger.Info("redirected default logger", |
||||
|
zap.String("from", currentDefaultLogWriterStr), |
||||
|
zap.String("to", newDefaultLogWriterStr), |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// closeLogs cleans up resources allocated during openLogs.
|
||||
|
// A successful call to openLogs calls this automatically
|
||||
|
// when the context is cancelled.
|
||||
|
func (logging *Logging) closeLogs() error { |
||||
|
for _, key := range logging.writerKeys { |
||||
|
_, err := writers.Delete(key) |
||||
|
if err != nil { |
||||
|
log.Printf("[ERROR] Closing log writer %v: %v", key, err) |
||||
|
} |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// Logger returns a logger that is ready for the module to use.
|
||||
|
func (logging *Logging) Logger(mod Module) *zap.Logger { |
||||
|
modName := mod.CaddyModule().Name |
||||
|
var cores []zapcore.Core |
||||
|
|
||||
|
if logging != nil { |
||||
|
for _, l := range logging.Logs { |
||||
|
if l.matchesModule(modName) { |
||||
|
if len(l.Include) == 0 && len(l.Exclude) == 0 { |
||||
|
cores = append(cores, l.core) |
||||
|
continue |
||||
|
} |
||||
|
cores = append(cores, &filteringCore{Core: l.core, cl: l}) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
multiCore := zapcore.NewTee(cores...) |
||||
|
|
||||
|
return zap.New(multiCore).Named(modName) |
||||
|
} |
||||
|
|
||||
|
// openWriter opens a writer using opener, and returns true if
|
||||
|
// the writer is new, or false if the writer already exists.
|
||||
|
func (logging *Logging) openWriter(opener WriterOpener) (io.WriteCloser, bool, error) { |
||||
|
key := opener.WriterKey() |
||||
|
writer, loaded, err := writers.LoadOrNew(key, func() (Destructor, error) { |
||||
|
w, err := opener.OpenWriter() |
||||
|
return writerDestructor{w}, err |
||||
|
}) |
||||
|
if err == nil { |
||||
|
logging.writerKeys = append(logging.writerKeys, key) |
||||
|
} |
||||
|
return writer.(io.WriteCloser), !loaded, err |
||||
|
} |
||||
|
|
||||
|
// WriterOpener is a module that can open a log writer.
|
||||
|
// It can return a human-readable string representation
|
||||
|
// of itself so that operators can understand where
|
||||
|
// the logs are going.
|
||||
|
type WriterOpener interface { |
||||
|
fmt.Stringer |
||||
|
|
||||
|
// WriterKey is a string that uniquely identifies this
|
||||
|
// writer configuration. It is not shown to humans.
|
||||
|
WriterKey() string |
||||
|
|
||||
|
// OpenWriter opens a log for writing. The writer
|
||||
|
// should be safe for concurrent use but need not
|
||||
|
// be synchronous.
|
||||
|
OpenWriter() (io.WriteCloser, error) |
||||
|
} |
||||
|
|
||||
|
type writerDestructor struct { |
||||
|
io.WriteCloser |
||||
|
} |
||||
|
|
||||
|
func (wdest writerDestructor) Destruct() error { |
||||
|
return wdest.Close() |
||||
|
} |
||||
|
|
||||
|
// StandardLibLog configures the default Go standard library
|
||||
|
// global logger in the log package. This is necessary because
|
||||
|
// module dependencies which are not built specifically for
|
||||
|
// Caddy will use the standard logger.
|
||||
|
type StandardLibLog struct { |
||||
|
WriterRaw json.RawMessage `json:"writer,omitempty"` |
||||
|
|
||||
|
writer io.WriteCloser |
||||
|
} |
||||
|
|
||||
|
func (sll *StandardLibLog) provision(ctx Context, logging *Logging) error { |
||||
|
if sll.WriterRaw != nil { |
||||
|
val, err := ctx.LoadModuleInline("output", "caddy.logging.writers", sll.WriterRaw) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("loading sink log writer module: %v", err) |
||||
|
} |
||||
|
wo := val.(WriterOpener) |
||||
|
sll.WriterRaw = nil // allow GC to deallocate
|
||||
|
|
||||
|
var isNew bool |
||||
|
sll.writer, isNew, err = logging.openWriter(wo) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("opening sink log writer %#v: %v", val, err) |
||||
|
} |
||||
|
|
||||
|
if isNew { |
||||
|
log.Printf("[INFO] Redirecting sink to: %s", wo) |
||||
|
log.SetOutput(sll.writer) |
||||
|
log.Printf("[INFO] Redirected sink to here (%s)", wo) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// CustomLog represents a custom logger configuration.
|
||||
|
type CustomLog struct { |
||||
|
WriterRaw json.RawMessage `json:"writer,omitempty"` |
||||
|
EncoderRaw json.RawMessage `json:"encoder,omitempty"` |
||||
|
Level string `json:"level,omitempty"` |
||||
|
Sampling *LogSampling `json:"sampling,omitempty"` |
||||
|
Include []string `json:"include,omitempty"` |
||||
|
Exclude []string `json:"exclude,omitempty"` |
||||
|
|
||||
|
writerOpener WriterOpener |
||||
|
writer io.WriteCloser |
||||
|
encoder zapcore.Encoder |
||||
|
levelEnabler zapcore.LevelEnabler |
||||
|
core zapcore.Core |
||||
|
} |
||||
|
|
||||
|
func (cl *CustomLog) provision(ctx Context, logging *Logging) error { |
||||
|
// set up the log level
|
||||
|
switch cl.Level { |
||||
|
case "debug": |
||||
|
cl.levelEnabler = zapcore.DebugLevel |
||||
|
case "", "info": |
||||
|
cl.levelEnabler = zapcore.InfoLevel |
||||
|
case "warn": |
||||
|
cl.levelEnabler = zapcore.WarnLevel |
||||
|
case "error": |
||||
|
cl.levelEnabler = zapcore.ErrorLevel |
||||
|
case "panic": |
||||
|
cl.levelEnabler = zapcore.PanicLevel |
||||
|
case "fatal": |
||||
|
cl.levelEnabler = zapcore.FatalLevel |
||||
|
default: |
||||
|
return fmt.Errorf("unrecognized log level: %s", cl.Level) |
||||
|
} |
||||
|
|
||||
|
// If both Include and Exclude lists are populated, then each item must
|
||||
|
// be a superspace or subspace of an item in the other list, because
|
||||
|
// populating both lists means that any given item is either a rule
|
||||
|
// or an exception to another rule. But if the item is not a super-
|
||||
|
// or sub-space of any item in the other list, it is neither a rule
|
||||
|
// nor an exception, and is a contradiction. Ensure, too, that the
|
||||
|
// sets do not intersect, which is also a contradiction.
|
||||
|
if len(cl.Include) > 0 && len(cl.Exclude) > 0 { |
||||
|
// prevent intersections
|
||||
|
for _, allow := range cl.Include { |
||||
|
for _, deny := range cl.Exclude { |
||||
|
if allow == deny { |
||||
|
return fmt.Errorf("include and exclude must not intersect, but found %s in both lists", allow) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// ensure namespaces are nested
|
||||
|
outer: |
||||
|
for _, allow := range cl.Include { |
||||
|
for _, deny := range cl.Exclude { |
||||
|
if strings.HasPrefix(allow+".", deny+".") || |
||||
|
strings.HasPrefix(deny+".", allow+".") { |
||||
|
continue outer |
||||
|
} |
||||
|
} |
||||
|
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) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if cl.EncoderRaw != nil { |
||||
|
val, err := ctx.LoadModuleInline("format", "caddy.logging.encoders", cl.EncoderRaw) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("loading log encoder module: %v", err) |
||||
|
} |
||||
|
cl.EncoderRaw = nil // allow GC to deallocate
|
||||
|
cl.encoder = val.(zapcore.Encoder) |
||||
|
} |
||||
|
if cl.encoder == nil { |
||||
|
cl.encoder = zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig()) |
||||
|
} |
||||
|
|
||||
|
if cl.WriterRaw != nil { |
||||
|
val, err := ctx.LoadModuleInline("output", "caddy.logging.writers", cl.WriterRaw) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("loading log writer module: %v", err) |
||||
|
} |
||||
|
cl.WriterRaw = nil // allow GC to deallocate
|
||||
|
cl.writerOpener = val.(WriterOpener) |
||||
|
} |
||||
|
if cl.writerOpener == nil { |
||||
|
cl.writerOpener = StderrWriter{} |
||||
|
} |
||||
|
var err error |
||||
|
cl.writer, _, err = logging.openWriter(cl.writerOpener) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("opening log writer using %#v: %v", cl.writerOpener, err) |
||||
|
} |
||||
|
|
||||
|
cl.buildCore() |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (cl *CustomLog) buildCore() { |
||||
|
// logs which only discard their output don't need
|
||||
|
// to perform encoding or any other processing steps
|
||||
|
// at all, so just shorcut to a nop core instead
|
||||
|
if _, ok := cl.writerOpener.(*DiscardWriter); ok { |
||||
|
cl.core = zapcore.NewNopCore() |
||||
|
return |
||||
|
} |
||||
|
c := zapcore.NewCore( |
||||
|
cl.encoder, |
||||
|
zapcore.AddSync(cl.writer), |
||||
|
cl.levelEnabler, |
||||
|
) |
||||
|
if cl.Sampling != nil { |
||||
|
if cl.Sampling.Interval == 0 { |
||||
|
cl.Sampling.Interval = 1 * time.Second |
||||
|
} |
||||
|
if cl.Sampling.First == 0 { |
||||
|
cl.Sampling.First = 100 |
||||
|
} |
||||
|
if cl.Sampling.Thereafter == 0 { |
||||
|
cl.Sampling.Thereafter = 100 |
||||
|
} |
||||
|
c = zapcore.NewSampler(c, cl.Sampling.Interval, |
||||
|
cl.Sampling.First, cl.Sampling.Thereafter) |
||||
|
} |
||||
|
cl.core = c |
||||
|
} |
||||
|
|
||||
|
func (cl *CustomLog) matchesModule(moduleName string) bool { |
||||
|
return cl.loggerAllowed(moduleName, true) |
||||
|
} |
||||
|
|
||||
|
// loggerAllowed returns true if name is allowed to emit
|
||||
|
// to cl. isModule should be true if name is the name of
|
||||
|
// a module and you want to see if ANY of that module's
|
||||
|
// logs would be permitted.
|
||||
|
func (cl *CustomLog) loggerAllowed(name string, isModule bool) bool { |
||||
|
// accept all loggers by default
|
||||
|
if len(cl.Include) == 0 && len(cl.Exclude) == 0 { |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
// append a dot so that partial names don't match
|
||||
|
// (i.e. we don't want "foo.b" to match "foo.bar"); we
|
||||
|
// will also have to append a dot when we do HasPrefix
|
||||
|
// below to compensate for when when namespaces are equal
|
||||
|
if name != "" && name != "*" && name != "." { |
||||
|
name += "." |
||||
|
} |
||||
|
|
||||
|
var longestAccept, longestReject int |
||||
|
|
||||
|
if len(cl.Include) > 0 { |
||||
|
for _, namespace := range cl.Include { |
||||
|
var hasPrefix bool |
||||
|
if isModule { |
||||
|
hasPrefix = strings.HasPrefix(namespace+".", name) |
||||
|
} else { |
||||
|
hasPrefix = strings.HasPrefix(name, namespace+".") |
||||
|
} |
||||
|
if hasPrefix && len(namespace) > longestAccept { |
||||
|
longestAccept = len(namespace) |
||||
|
} |
||||
|
} |
||||
|
// the include list was populated, meaning that
|
||||
|
// a match in this list is absolutely required
|
||||
|
// if we are to accept the entry
|
||||
|
if longestAccept == 0 { |
||||
|
return false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if len(cl.Exclude) > 0 { |
||||
|
for _, namespace := range cl.Exclude { |
||||
|
// * == all logs emitted by modules
|
||||
|
// . == all logs emitted by core
|
||||
|
if (namespace == "*" && name != ".") || |
||||
|
(namespace == "." && name == ".") { |
||||
|
return false |
||||
|
} |
||||
|
if strings.HasPrefix(name, namespace+".") && |
||||
|
len(namespace) > longestReject { |
||||
|
longestReject = len(namespace) |
||||
|
} |
||||
|
} |
||||
|
// the reject list is populated, so we have to
|
||||
|
// reject this entry if its match is better
|
||||
|
// than the best from the accept list
|
||||
|
if longestReject > longestAccept { |
||||
|
return false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return (longestAccept > longestReject) || |
||||
|
(len(cl.Include) == 0 && longestReject == 0) |
||||
|
} |
||||
|
|
||||
|
// filteringCore filters log entries based on logger name,
|
||||
|
// according to the rules of a CustomLog.
|
||||
|
type filteringCore struct { |
||||
|
zapcore.Core |
||||
|
cl *CustomLog |
||||
|
} |
||||
|
|
||||
|
// With properly wraps With.
|
||||
|
func (fc *filteringCore) With(fields []zapcore.Field) zapcore.Core { |
||||
|
return &filteringCore{ |
||||
|
Core: fc.Core.With(fields), |
||||
|
cl: fc.cl, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Check only allows the log entry if its logger name
|
||||
|
// is allowed from the include/exclude rules of fc.cl.
|
||||
|
func (fc *filteringCore) Check(e zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry { |
||||
|
if fc.cl.loggerAllowed(e.LoggerName, false) { |
||||
|
return fc.Core.Check(e, ce) |
||||
|
} |
||||
|
return ce |
||||
|
} |
||||
|
|
||||
|
// LogSampling configures log entry sampling.
|
||||
|
type LogSampling struct { |
||||
|
Interval time.Duration `json:"interval,omitempty"` |
||||
|
First int `json:"first,omitempty"` |
||||
|
Thereafter int `json:"thereafter,omitempty"` |
||||
|
} |
||||
|
|
||||
|
type ( |
||||
|
// StdoutWriter can write logs to stdout.
|
||||
|
StdoutWriter struct{} |
||||
|
|
||||
|
// StderrWriter can write logs to stdout.
|
||||
|
StderrWriter struct{} |
||||
|
|
||||
|
// DiscardWriter discards all writes.
|
||||
|
DiscardWriter struct{} |
||||
|
) |
||||
|
|
||||
|
// CaddyModule returns the Caddy module information.
|
||||
|
func (StdoutWriter) CaddyModule() ModuleInfo { |
||||
|
return ModuleInfo{ |
||||
|
Name: "caddy.logging.writers.stdout", |
||||
|
New: func() Module { return new(StdoutWriter) }, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// CaddyModule returns the Caddy module information.
|
||||
|
func (StderrWriter) CaddyModule() ModuleInfo { |
||||
|
return ModuleInfo{ |
||||
|
Name: "caddy.logging.writers.stderr", |
||||
|
New: func() Module { return new(StderrWriter) }, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// CaddyModule returns the Caddy module information.
|
||||
|
func (DiscardWriter) CaddyModule() ModuleInfo { |
||||
|
return ModuleInfo{ |
||||
|
Name: "caddy.logging.writers.discard", |
||||
|
New: func() Module { return new(DiscardWriter) }, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (StdoutWriter) String() string { return "stdout" } |
||||
|
func (StderrWriter) String() string { return "stderr" } |
||||
|
func (DiscardWriter) String() string { return "discard" } |
||||
|
|
||||
|
// WriterKey returns a unique key representing stdout.
|
||||
|
func (StdoutWriter) WriterKey() string { return "std:out" } |
||||
|
|
||||
|
// WriterKey returns a unique key representing stderr.
|
||||
|
func (StderrWriter) WriterKey() string { return "std:err" } |
||||
|
|
||||
|
// WriterKey returns a unique key representing discard.
|
||||
|
func (DiscardWriter) WriterKey() string { return "discard" } |
||||
|
|
||||
|
// OpenWriter returns os.Stdout that can't be closed.
|
||||
|
func (StdoutWriter) OpenWriter() (io.WriteCloser, error) { |
||||
|
return notClosable{os.Stdout}, nil |
||||
|
} |
||||
|
|
||||
|
// OpenWriter returns os.Stderr that can't be closed.
|
||||
|
func (StderrWriter) OpenWriter() (io.WriteCloser, error) { |
||||
|
return notClosable{os.Stderr}, nil |
||||
|
} |
||||
|
|
||||
|
// OpenWriter returns ioutil.Discard that can't be closed.
|
||||
|
func (DiscardWriter) OpenWriter() (io.WriteCloser, error) { |
||||
|
return notClosable{ioutil.Discard}, nil |
||||
|
} |
||||
|
|
||||
|
// notClosable is an io.WriteCloser that can't be closed.
|
||||
|
type notClosable struct{ io.Writer } |
||||
|
|
||||
|
func (fc notClosable) Close() error { return nil } |
||||
|
|
||||
|
type defaultCustomLog struct { |
||||
|
*CustomLog |
||||
|
logger *zap.Logger |
||||
|
} |
||||
|
|
||||
|
// newDefaultProductionLog configures a custom log that is
|
||||
|
// intended for use by default if no other log is specified
|
||||
|
// in a config. It writes to stderr, uses the console encoder,
|
||||
|
// and enables INFO-level logs and higher.
|
||||
|
func newDefaultProductionLog() (*defaultCustomLog, error) { |
||||
|
cl := new(CustomLog) |
||||
|
cl.writerOpener = StderrWriter{} |
||||
|
var err error |
||||
|
cl.writer, err = cl.writerOpener.OpenWriter() |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
encCfg := zap.NewProductionEncoderConfig() |
||||
|
cl.encoder = zapcore.NewConsoleEncoder(encCfg) |
||||
|
cl.levelEnabler = zapcore.InfoLevel |
||||
|
|
||||
|
cl.buildCore() |
||||
|
|
||||
|
return &defaultCustomLog{ |
||||
|
CustomLog: cl, |
||||
|
logger: zap.New(cl.core), |
||||
|
}, nil |
||||
|
} |
||||
|
|
||||
|
// Log returns the current default logger.
|
||||
|
func Log() *zap.Logger { |
||||
|
defaultLoggerMu.RLock() |
||||
|
defer defaultLoggerMu.RUnlock() |
||||
|
return defaultLogger.logger |
||||
|
} |
||||
|
|
||||
|
var ( |
||||
|
defaultLogger, _ = newDefaultProductionLog() |
||||
|
defaultLoggerMu sync.RWMutex |
||||
|
) |
||||
|
|
||||
|
var writers = NewUsagePool() |
||||
|
|
||||
|
// Interface guards
|
||||
|
var ( |
||||
|
_ io.WriteCloser = (*notClosable)(nil) |
||||
|
_ WriterOpener = (*StdoutWriter)(nil) |
||||
|
_ WriterOpener = (*StderrWriter)(nil) |
||||
|
) |
@ -0,0 +1,89 @@ |
|||||
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
|
//
|
||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
// you may not use this file except in compliance with the License.
|
||||
|
// You may obtain a copy of the License at
|
||||
|
//
|
||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
//
|
||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
// See the License for the specific language governing permissions and
|
||||
|
// limitations under the License.
|
||||
|
|
||||
|
package caddyhttp |
||||
|
|
||||
|
import ( |
||||
|
"crypto/tls" |
||||
|
"net/http" |
||||
|
|
||||
|
"go.uber.org/zap/zapcore" |
||||
|
) |
||||
|
|
||||
|
// LoggableHTTPRequest makes an HTTP request loggable with zap.Object().
|
||||
|
type LoggableHTTPRequest struct{ *http.Request } |
||||
|
|
||||
|
// MarshalLogObject satisfies the zapcore.ObjectMarshaler interface.
|
||||
|
func (r LoggableHTTPRequest) MarshalLogObject(enc zapcore.ObjectEncoder) error { |
||||
|
enc.AddString("method", r.Method) |
||||
|
enc.AddString("uri", r.RequestURI) |
||||
|
enc.AddString("proto", r.Proto) |
||||
|
enc.AddString("remote_addr", r.RemoteAddr) |
||||
|
enc.AddString("host", r.Host) |
||||
|
enc.AddObject("headers", LoggableHTTPHeader(r.Header)) |
||||
|
if r.TLS != nil { |
||||
|
enc.AddObject("tls", LoggableTLSConnState(*r.TLS)) |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// LoggableHTTPHeader makes an HTTP header loggable with zap.Object().
|
||||
|
type LoggableHTTPHeader http.Header |
||||
|
|
||||
|
// MarshalLogObject satisfies the zapcore.ObjectMarshaler interface.
|
||||
|
func (h LoggableHTTPHeader) MarshalLogObject(enc zapcore.ObjectEncoder) error { |
||||
|
if h == nil { |
||||
|
return nil |
||||
|
} |
||||
|
for key, val := range h { |
||||
|
enc.AddArray(key, LoggableStringArray(val)) |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// LoggableStringArray makes a slice of strings marshalable for logging.
|
||||
|
type LoggableStringArray []string |
||||
|
|
||||
|
// MarshalLogArray satisfies the zapcore.ArrayMarshaler interface.
|
||||
|
func (sa LoggableStringArray) MarshalLogArray(enc zapcore.ArrayEncoder) error { |
||||
|
if sa == nil { |
||||
|
return nil |
||||
|
} |
||||
|
for _, s := range sa { |
||||
|
enc.AppendString(s) |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// LoggableTLSConnState makes a TLS connection state loggable with zap.Object().
|
||||
|
type LoggableTLSConnState tls.ConnectionState |
||||
|
|
||||
|
// MarshalLogObject satisfies the zapcore.ObjectMarshaler interface.
|
||||
|
func (t LoggableTLSConnState) MarshalLogObject(enc zapcore.ObjectEncoder) error { |
||||
|
enc.AddBool("resumed", t.DidResume) |
||||
|
enc.AddUint16("version", t.Version) |
||||
|
enc.AddUint16("resumed", t.CipherSuite) |
||||
|
enc.AddString("proto", t.NegotiatedProtocol) |
||||
|
enc.AddBool("proto_mutual", t.NegotiatedProtocolIsMutual) |
||||
|
enc.AddString("server_name", t.ServerName) |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// Interface guards
|
||||
|
var ( |
||||
|
_ zapcore.ObjectMarshaler = (*LoggableHTTPRequest)(nil) |
||||
|
_ zapcore.ObjectMarshaler = (*LoggableHTTPHeader)(nil) |
||||
|
_ zapcore.ArrayMarshaler = (*LoggableStringArray)(nil) |
||||
|
_ zapcore.ObjectMarshaler = (*LoggableTLSConnState)(nil) |
||||
|
) |
@ -0,0 +1,268 @@ |
|||||
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
|
//
|
||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
// you may not use this file except in compliance with the License.
|
||||
|
// You may obtain a copy of the License at
|
||||
|
//
|
||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
//
|
||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
// See the License for the specific language governing permissions and
|
||||
|
// limitations under the License.
|
||||
|
|
||||
|
package logging |
||||
|
|
||||
|
import ( |
||||
|
"encoding/json" |
||||
|
"fmt" |
||||
|
"strings" |
||||
|
"time" |
||||
|
|
||||
|
"github.com/caddyserver/caddy/v2" |
||||
|
zaplogfmt "github.com/jsternberg/zap-logfmt" |
||||
|
"go.uber.org/zap" |
||||
|
"go.uber.org/zap/buffer" |
||||
|
"go.uber.org/zap/zapcore" |
||||
|
) |
||||
|
|
||||
|
func init() { |
||||
|
caddy.RegisterModule(ConsoleEncoder{}) |
||||
|
caddy.RegisterModule(JSONEncoder{}) |
||||
|
caddy.RegisterModule(LogfmtEncoder{}) |
||||
|
caddy.RegisterModule(StringEncoder{}) |
||||
|
} |
||||
|
|
||||
|
// ConsoleEncoder encodes log entries that are mostly human-readable.
|
||||
|
type ConsoleEncoder struct { |
||||
|
zapcore.Encoder |
||||
|
LogEncoderConfig |
||||
|
} |
||||
|
|
||||
|
// CaddyModule returns the Caddy module information.
|
||||
|
func (ConsoleEncoder) CaddyModule() caddy.ModuleInfo { |
||||
|
return caddy.ModuleInfo{ |
||||
|
Name: "caddy.logging.encoders.console", |
||||
|
New: func() caddy.Module { return new(ConsoleEncoder) }, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Provision sets up the encoder.
|
||||
|
func (ce *ConsoleEncoder) Provision(_ caddy.Context) error { |
||||
|
ce.Encoder = zapcore.NewConsoleEncoder(ce.ZapcoreEncoderConfig()) |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// JSONEncoder encodes entries as JSON.
|
||||
|
type JSONEncoder struct { |
||||
|
zapcore.Encoder |
||||
|
*LogEncoderConfig |
||||
|
} |
||||
|
|
||||
|
// CaddyModule returns the Caddy module information.
|
||||
|
func (JSONEncoder) CaddyModule() caddy.ModuleInfo { |
||||
|
return caddy.ModuleInfo{ |
||||
|
Name: "caddy.logging.encoders.json", |
||||
|
New: func() caddy.Module { return new(JSONEncoder) }, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Provision sets up the encoder.
|
||||
|
func (je *JSONEncoder) Provision(_ caddy.Context) error { |
||||
|
je.Encoder = zapcore.NewJSONEncoder(je.ZapcoreEncoderConfig()) |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// LogfmtEncoder encodes log entries as logfmt:
|
||||
|
// https://www.brandur.org/logfmt
|
||||
|
type LogfmtEncoder struct { |
||||
|
zapcore.Encoder |
||||
|
LogEncoderConfig |
||||
|
} |
||||
|
|
||||
|
// CaddyModule returns the Caddy module information.
|
||||
|
func (LogfmtEncoder) CaddyModule() caddy.ModuleInfo { |
||||
|
return caddy.ModuleInfo{ |
||||
|
Name: "caddy.logging.encoders.logfmt", |
||||
|
New: func() caddy.Module { return new(LogfmtEncoder) }, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Provision sets up the encoder.
|
||||
|
func (lfe *LogfmtEncoder) Provision(_ caddy.Context) error { |
||||
|
lfe.Encoder = zaplogfmt.NewEncoder(lfe.ZapcoreEncoderConfig()) |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// StringEncoder writes a log entry that consists entirely
|
||||
|
// of a single string field in the log entry. This is useful
|
||||
|
// for custom, self-encoded log entries that consist of a
|
||||
|
// single field in the structured log entry.
|
||||
|
type StringEncoder struct { |
||||
|
zapcore.Encoder |
||||
|
FieldName string `json:"field,omitempty"` |
||||
|
FallbackRaw json.RawMessage `json:"fallback,omitempty"` |
||||
|
} |
||||
|
|
||||
|
// CaddyModule returns the Caddy module information.
|
||||
|
func (StringEncoder) CaddyModule() caddy.ModuleInfo { |
||||
|
return caddy.ModuleInfo{ |
||||
|
Name: "caddy.logging.encoders.string", |
||||
|
New: func() caddy.Module { return new(StringEncoder) }, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Provision sets up the encoder.
|
||||
|
func (se *StringEncoder) Provision(ctx caddy.Context) error { |
||||
|
if se.FallbackRaw != nil { |
||||
|
val, err := ctx.LoadModuleInline("format", "caddy.logging.encoders", se.FallbackRaw) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("loading fallback encoder module: %v", err) |
||||
|
} |
||||
|
se.FallbackRaw = nil // allow GC to deallocate
|
||||
|
se.Encoder = val.(zapcore.Encoder) |
||||
|
} |
||||
|
if se.Encoder == nil { |
||||
|
se.Encoder = nopEncoder{} |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// Clone wraps the underlying encoder's Clone. This is
|
||||
|
// necessary because we implement our own EncodeEntry,
|
||||
|
// and if we simply let the embedded encoder's Clone
|
||||
|
// be promoted, it would return a clone of that, and
|
||||
|
// we'd lose our StringEncoder's EncodeEntry.
|
||||
|
func (se StringEncoder) Clone() zapcore.Encoder { |
||||
|
return StringEncoder{ |
||||
|
Encoder: se.Encoder.Clone(), |
||||
|
FieldName: se.FieldName, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// EncodeEntry partially implements the zapcore.Encoder interface.
|
||||
|
func (se StringEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) { |
||||
|
for _, f := range fields { |
||||
|
if f.Key == se.FieldName { |
||||
|
buf := bufferpool.Get() |
||||
|
buf.AppendString(f.String) |
||||
|
if !strings.HasSuffix(f.String, "\n") { |
||||
|
buf.AppendByte('\n') |
||||
|
} |
||||
|
return buf, nil |
||||
|
} |
||||
|
} |
||||
|
if se.Encoder == nil { |
||||
|
return nil, fmt.Errorf("no fallback encoder defined") |
||||
|
} |
||||
|
return se.Encoder.EncodeEntry(ent, fields) |
||||
|
} |
||||
|
|
||||
|
// LogEncoderConfig holds configuration common to most encoders.
|
||||
|
type LogEncoderConfig struct { |
||||
|
MessageKey *string `json:"message_key,omitempty"` |
||||
|
LevelKey *string `json:"level_key,omitempty"` |
||||
|
TimeKey *string `json:"time_key,omitempty"` |
||||
|
NameKey *string `json:"name_key,omitempty"` |
||||
|
CallerKey *string `json:"caller_key,omitempty"` |
||||
|
StacktraceKey *string `json:"stacktrace_key,omitempty"` |
||||
|
LineEnding *string `json:"line_ending,omitempty"` |
||||
|
TimeFormat string `json:"time_format,omitempty"` |
||||
|
DurationFormat string `json:"duration_format,omitempty"` |
||||
|
LevelFormat string `json:"level_format,omitempty"` |
||||
|
} |
||||
|
|
||||
|
// ZapcoreEncoderConfig returns the equivalent zapcore.EncoderConfig.
|
||||
|
// If lec is nil, zap.NewProductionEncoderConfig() is returned.
|
||||
|
func (lec *LogEncoderConfig) ZapcoreEncoderConfig() zapcore.EncoderConfig { |
||||
|
cfg := zap.NewProductionEncoderConfig() |
||||
|
if lec == nil { |
||||
|
lec = new(LogEncoderConfig) |
||||
|
} |
||||
|
if lec.MessageKey != nil { |
||||
|
cfg.MessageKey = *lec.MessageKey |
||||
|
} |
||||
|
if lec.TimeKey != nil { |
||||
|
cfg.TimeKey = *lec.TimeKey |
||||
|
} |
||||
|
if lec.NameKey != nil { |
||||
|
cfg.NameKey = *lec.NameKey |
||||
|
} |
||||
|
if lec.CallerKey != nil { |
||||
|
cfg.CallerKey = *lec.CallerKey |
||||
|
} |
||||
|
if lec.StacktraceKey != nil { |
||||
|
cfg.StacktraceKey = *lec.StacktraceKey |
||||
|
} |
||||
|
if lec.LineEnding != nil { |
||||
|
cfg.LineEnding = *lec.LineEnding |
||||
|
} |
||||
|
|
||||
|
// time format
|
||||
|
var timeFormatter zapcore.TimeEncoder |
||||
|
switch lec.TimeFormat { |
||||
|
case "", "unix_seconds_float": |
||||
|
timeFormatter = zapcore.EpochTimeEncoder |
||||
|
case "unix_milli_float": |
||||
|
timeFormatter = zapcore.EpochMillisTimeEncoder |
||||
|
case "unix_nano": |
||||
|
timeFormatter = zapcore.EpochNanosTimeEncoder |
||||
|
case "iso8601": |
||||
|
timeFormatter = zapcore.ISO8601TimeEncoder |
||||
|
default: |
||||
|
timeFormat := lec.TimeFormat |
||||
|
switch lec.TimeFormat { |
||||
|
case "rfc3339": |
||||
|
timeFormat = time.RFC3339 |
||||
|
case "rfc3339_nano": |
||||
|
timeFormat = time.RFC3339Nano |
||||
|
case "wall": |
||||
|
timeFormat = "2006/01/02 15:04:05" |
||||
|
case "wall_milli": |
||||
|
timeFormat = "2006/01/02 15:04:05.000" |
||||
|
case "wall_nano": |
||||
|
timeFormat = "2006/01/02 15:04:05.000000000" |
||||
|
} |
||||
|
timeFormatter = func(ts time.Time, encoder zapcore.PrimitiveArrayEncoder) { |
||||
|
encoder.AppendString(ts.UTC().Format(timeFormat)) |
||||
|
} |
||||
|
} |
||||
|
cfg.EncodeTime = timeFormatter |
||||
|
|
||||
|
// duration format
|
||||
|
var durFormatter zapcore.DurationEncoder |
||||
|
switch lec.DurationFormat { |
||||
|
case "", "seconds": |
||||
|
durFormatter = zapcore.SecondsDurationEncoder |
||||
|
case "nano": |
||||
|
durFormatter = zapcore.NanosDurationEncoder |
||||
|
case "string": |
||||
|
durFormatter = zapcore.StringDurationEncoder |
||||
|
} |
||||
|
cfg.EncodeDuration = durFormatter |
||||
|
|
||||
|
// level format
|
||||
|
var levelFormatter zapcore.LevelEncoder |
||||
|
switch lec.LevelFormat { |
||||
|
case "", "lower": |
||||
|
levelFormatter = zapcore.LowercaseLevelEncoder |
||||
|
case "upper": |
||||
|
levelFormatter = zapcore.CapitalLevelEncoder |
||||
|
case "color": |
||||
|
levelFormatter = zapcore.CapitalColorLevelEncoder |
||||
|
} |
||||
|
cfg.EncodeLevel = levelFormatter |
||||
|
|
||||
|
return cfg |
||||
|
} |
||||
|
|
||||
|
var bufferpool = buffer.NewPool() |
||||
|
|
||||
|
// Interface guards
|
||||
|
var ( |
||||
|
_ zapcore.Encoder = (*ConsoleEncoder)(nil) |
||||
|
_ zapcore.Encoder = (*JSONEncoder)(nil) |
||||
|
_ zapcore.Encoder = (*LogfmtEncoder)(nil) |
||||
|
_ zapcore.Encoder = (*StringEncoder)(nil) |
||||
|
) |
@ -0,0 +1,91 @@ |
|||||
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
|
//
|
||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
// you may not use this file except in compliance with the License.
|
||||
|
// You may obtain a copy of the License at
|
||||
|
//
|
||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
//
|
||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
// See the License for the specific language governing permissions and
|
||||
|
// limitations under the License.
|
||||
|
|
||||
|
package logging |
||||
|
|
||||
|
import ( |
||||
|
"io" |
||||
|
"os" |
||||
|
"path/filepath" |
||||
|
|
||||
|
"github.com/caddyserver/caddy/v2" |
||||
|
"gopkg.in/natefinch/lumberjack.v2" |
||||
|
) |
||||
|
|
||||
|
func init() { |
||||
|
caddy.RegisterModule(FileWriter{}) |
||||
|
} |
||||
|
|
||||
|
// FileWriter can write logs to files.
|
||||
|
type FileWriter struct { |
||||
|
Filename string `json:"filename,omitempty"` |
||||
|
Roll *bool `json:"roll,omitempty"` |
||||
|
RollSizeMB int `json:"roll_size_mb,omitempty"` |
||||
|
RollCompress *bool `json:"roll_gzip,omitempty"` |
||||
|
RollLocalTime bool `json:"roll_local_time,omitempty"` |
||||
|
RollKeep int `json:"roll_keep,omitempty"` |
||||
|
RollKeepDays int `json:"roll_keep_days,omitempty"` |
||||
|
} |
||||
|
|
||||
|
// CaddyModule returns the Caddy module information.
|
||||
|
func (FileWriter) CaddyModule() caddy.ModuleInfo { |
||||
|
return caddy.ModuleInfo{ |
||||
|
Name: "caddy.logging.writers.file", |
||||
|
New: func() caddy.Module { return new(FileWriter) }, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (fw FileWriter) String() string { |
||||
|
fpath, err := filepath.Abs(fw.Filename) |
||||
|
if err == nil { |
||||
|
return fpath |
||||
|
} |
||||
|
return fw.Filename |
||||
|
} |
||||
|
|
||||
|
// WriterKey returns a unique key representing this fw.
|
||||
|
func (fw FileWriter) WriterKey() string { |
||||
|
return "file:" + fw.Filename |
||||
|
} |
||||
|
|
||||
|
// OpenWriter opens a new file writer.
|
||||
|
func (fw FileWriter) OpenWriter() (io.WriteCloser, error) { |
||||
|
// roll log files by default
|
||||
|
if fw.Roll == nil || *fw.Roll == true { |
||||
|
if fw.RollSizeMB == 0 { |
||||
|
fw.RollSizeMB = 100 |
||||
|
} |
||||
|
if fw.RollCompress == nil { |
||||
|
compress := true |
||||
|
fw.RollCompress = &compress |
||||
|
} |
||||
|
if fw.RollKeep == 0 { |
||||
|
fw.RollKeep = 10 |
||||
|
} |
||||
|
if fw.RollKeepDays == 0 { |
||||
|
fw.RollKeepDays = 90 |
||||
|
} |
||||
|
return &lumberjack.Logger{ |
||||
|
Filename: fw.Filename, |
||||
|
MaxSize: fw.RollSizeMB, |
||||
|
MaxAge: fw.RollKeepDays, |
||||
|
MaxBackups: fw.RollKeep, |
||||
|
LocalTime: fw.RollLocalTime, |
||||
|
Compress: *fw.RollCompress, |
||||
|
}, nil |
||||
|
} |
||||
|
|
||||
|
// otherwise just open a regular file
|
||||
|
return os.OpenFile(fw.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666) |
||||
|
} |
@ -0,0 +1,321 @@ |
|||||
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
|
//
|
||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
// you may not use this file except in compliance with the License.
|
||||
|
// You may obtain a copy of the License at
|
||||
|
//
|
||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
//
|
||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
// See the License for the specific language governing permissions and
|
||||
|
// limitations under the License.
|
||||
|
|
||||
|
package logging |
||||
|
|
||||
|
import ( |
||||
|
"encoding/json" |
||||
|
"fmt" |
||||
|
"time" |
||||
|
|
||||
|
"github.com/caddyserver/caddy/v2" |
||||
|
"go.uber.org/zap" |
||||
|
"go.uber.org/zap/buffer" |
||||
|
"go.uber.org/zap/zapcore" |
||||
|
) |
||||
|
|
||||
|
func init() { |
||||
|
caddy.RegisterModule(FilterEncoder{}) |
||||
|
} |
||||
|
|
||||
|
// FilterEncoder wraps an underlying encoder. It does
|
||||
|
// not do any encoding itself, but it can manipulate
|
||||
|
// (filter) fields before they are actually encoded.
|
||||
|
// A wrapped encoder is required.
|
||||
|
type FilterEncoder struct { |
||||
|
WrappedRaw json.RawMessage `json:"wrap,omitempty"` |
||||
|
FieldsRaw map[string]json.RawMessage `json:"fields,omitempty"` |
||||
|
|
||||
|
wrapped zapcore.Encoder |
||||
|
Fields map[string]LogFieldFilter `json:"-"` |
||||
|
|
||||
|
// used to keep keys unique across nested objects
|
||||
|
keyPrefix string |
||||
|
} |
||||
|
|
||||
|
// CaddyModule returns the Caddy module information.
|
||||
|
func (FilterEncoder) CaddyModule() caddy.ModuleInfo { |
||||
|
return caddy.ModuleInfo{ |
||||
|
Name: "caddy.logging.encoders.filter", |
||||
|
New: func() caddy.Module { return new(FilterEncoder) }, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Provision sets up the encoder.
|
||||
|
func (fe *FilterEncoder) Provision(ctx caddy.Context) error { |
||||
|
if fe.WrappedRaw == nil { |
||||
|
return fmt.Errorf("missing \"wrap\" (must specify an underlying encoder)") |
||||
|
} |
||||
|
|
||||
|
// set up wrapped encoder (required)
|
||||
|
val, err := ctx.LoadModuleInline("format", "caddy.logging.encoders", fe.WrappedRaw) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("loading fallback encoder module: %v", err) |
||||
|
} |
||||
|
fe.WrappedRaw = nil // allow GC to deallocate
|
||||
|
fe.wrapped = val.(zapcore.Encoder) |
||||
|
|
||||
|
// set up each field filter
|
||||
|
if fe.Fields == nil { |
||||
|
fe.Fields = make(map[string]LogFieldFilter) |
||||
|
} |
||||
|
for field, filterRaw := range fe.FieldsRaw { |
||||
|
if filterRaw == nil { |
||||
|
continue |
||||
|
} |
||||
|
val, err := ctx.LoadModuleInline("filter", "caddy.logging.encoders.filter", filterRaw) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("loading log filter module: %v", err) |
||||
|
} |
||||
|
fe.Fields[field] = val.(LogFieldFilter) |
||||
|
} |
||||
|
fe.FieldsRaw = nil // allow GC to deallocate
|
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// AddArray is part of the zapcore.ObjectEncoder interface.
|
||||
|
// Array elements do not get filtered.
|
||||
|
func (fe FilterEncoder) AddArray(key string, marshaler zapcore.ArrayMarshaler) error { |
||||
|
if filter, ok := fe.Fields[fe.keyPrefix+key]; ok { |
||||
|
filter.Filter(zap.Array(key, marshaler)).AddTo(fe.wrapped) |
||||
|
return nil |
||||
|
} |
||||
|
return fe.wrapped.AddArray(key, marshaler) |
||||
|
} |
||||
|
|
||||
|
// AddObject is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (fe FilterEncoder) AddObject(key string, marshaler zapcore.ObjectMarshaler) error { |
||||
|
fe.keyPrefix += key + ">" |
||||
|
return fe.wrapped.AddObject(key, logObjectMarshalerWrapper{ |
||||
|
enc: fe, |
||||
|
marsh: marshaler, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// AddBinary is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (fe FilterEncoder) AddBinary(key string, value []byte) { |
||||
|
if !fe.filtered(key, value) { |
||||
|
fe.wrapped.AddBinary(key, value) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// AddByteString is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (fe FilterEncoder) AddByteString(key string, value []byte) { |
||||
|
if !fe.filtered(key, value) { |
||||
|
fe.wrapped.AddByteString(key, value) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// AddBool is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (fe FilterEncoder) AddBool(key string, value bool) { |
||||
|
if !fe.filtered(key, value) { |
||||
|
fe.wrapped.AddBool(key, value) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// AddComplex128 is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (fe FilterEncoder) AddComplex128(key string, value complex128) { |
||||
|
if !fe.filtered(key, value) { |
||||
|
fe.wrapped.AddComplex128(key, value) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// AddComplex64 is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (fe FilterEncoder) AddComplex64(key string, value complex64) { |
||||
|
if !fe.filtered(key, value) { |
||||
|
fe.wrapped.AddComplex64(key, value) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// AddDuration is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (fe FilterEncoder) AddDuration(key string, value time.Duration) { |
||||
|
if !fe.filtered(key, value) { |
||||
|
fe.wrapped.AddDuration(key, value) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// AddFloat64 is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (fe FilterEncoder) AddFloat64(key string, value float64) { |
||||
|
if !fe.filtered(key, value) { |
||||
|
fe.wrapped.AddFloat64(key, value) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// AddFloat32 is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (fe FilterEncoder) AddFloat32(key string, value float32) { |
||||
|
if !fe.filtered(key, value) { |
||||
|
fe.wrapped.AddFloat32(key, value) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// AddInt is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (fe FilterEncoder) AddInt(key string, value int) { |
||||
|
if !fe.filtered(key, value) { |
||||
|
fe.wrapped.AddInt(key, value) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// AddInt64 is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (fe FilterEncoder) AddInt64(key string, value int64) { |
||||
|
if !fe.filtered(key, value) { |
||||
|
fe.wrapped.AddInt64(key, value) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// AddInt32 is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (fe FilterEncoder) AddInt32(key string, value int32) { |
||||
|
if !fe.filtered(key, value) { |
||||
|
fe.wrapped.AddInt32(key, value) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// AddInt16 is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (fe FilterEncoder) AddInt16(key string, value int16) { |
||||
|
if !fe.filtered(key, value) { |
||||
|
fe.wrapped.AddInt16(key, value) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// AddInt8 is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (fe FilterEncoder) AddInt8(key string, value int8) { |
||||
|
if !fe.filtered(key, value) { |
||||
|
fe.wrapped.AddInt8(key, value) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// AddString is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (fe FilterEncoder) AddString(key, value string) { |
||||
|
if !fe.filtered(key, value) { |
||||
|
fe.wrapped.AddString(key, value) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// AddTime is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (fe FilterEncoder) AddTime(key string, value time.Time) { |
||||
|
if !fe.filtered(key, value) { |
||||
|
fe.wrapped.AddTime(key, value) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// AddUint is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (fe FilterEncoder) AddUint(key string, value uint) { |
||||
|
if !fe.filtered(key, value) { |
||||
|
fe.wrapped.AddUint(key, value) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// AddUint64 is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (fe FilterEncoder) AddUint64(key string, value uint64) { |
||||
|
if !fe.filtered(key, value) { |
||||
|
fe.wrapped.AddUint64(key, value) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// AddUint32 is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (fe FilterEncoder) AddUint32(key string, value uint32) { |
||||
|
if !fe.filtered(key, value) { |
||||
|
fe.wrapped.AddUint32(key, value) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// AddUint16 is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (fe FilterEncoder) AddUint16(key string, value uint16) { |
||||
|
if !fe.filtered(key, value) { |
||||
|
fe.wrapped.AddUint16(key, value) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// AddUint8 is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (fe FilterEncoder) AddUint8(key string, value uint8) { |
||||
|
if !fe.filtered(key, value) { |
||||
|
fe.wrapped.AddUint8(key, value) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// AddUintptr is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (fe FilterEncoder) AddUintptr(key string, value uintptr) { |
||||
|
if !fe.filtered(key, value) { |
||||
|
fe.wrapped.AddUintptr(key, value) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// AddReflected is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (fe FilterEncoder) AddReflected(key string, value interface{}) error { |
||||
|
if !fe.filtered(key, value) { |
||||
|
return fe.wrapped.AddReflected(key, value) |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// OpenNamespace is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (fe FilterEncoder) OpenNamespace(key string) { |
||||
|
fe.wrapped.OpenNamespace(key) |
||||
|
} |
||||
|
|
||||
|
// Clone is part of the zapcore.ObjectEncoder interface.
|
||||
|
// We don't use it as of Oct 2019 (v2 beta 7), I'm not
|
||||
|
// really sure what it'd be useful for in our case.
|
||||
|
func (fe FilterEncoder) Clone() zapcore.Encoder { |
||||
|
return FilterEncoder{ |
||||
|
Fields: fe.Fields, |
||||
|
wrapped: fe.wrapped.Clone(), |
||||
|
keyPrefix: fe.keyPrefix, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// EncodeEntry partially implements the zapcore.Encoder interface.
|
||||
|
func (fe FilterEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) { |
||||
|
// without this clone and storing it to fe.wrapped, fields
|
||||
|
// from subsequent log entries get appended to previous
|
||||
|
// ones, and I'm not 100% sure why; see end of
|
||||
|
// https://github.com/uber-go/zap/issues/750
|
||||
|
fe.wrapped = fe.wrapped.Clone() |
||||
|
for _, field := range fields { |
||||
|
field.AddTo(fe) |
||||
|
} |
||||
|
return fe.wrapped.EncodeEntry(ent, nil) |
||||
|
} |
||||
|
|
||||
|
// filtered returns true if the field was filtered.
|
||||
|
// If true is returned, the field was filtered and
|
||||
|
// added to the underlying encoder (so do not do
|
||||
|
// that again). If false was returned, the field has
|
||||
|
// not yet been added to the underlying encoder.
|
||||
|
func (fe FilterEncoder) filtered(key string, value interface{}) bool { |
||||
|
filter, ok := fe.Fields[fe.keyPrefix+key] |
||||
|
if !ok { |
||||
|
return false |
||||
|
} |
||||
|
filter.Filter(zap.Any(key, value)).AddTo(fe.wrapped) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
// logObjectMarshalerWrapper allows us to recursively
|
||||
|
// filter fields of objects as they get encoded.
|
||||
|
type logObjectMarshalerWrapper struct { |
||||
|
enc FilterEncoder |
||||
|
marsh zapcore.ObjectMarshaler |
||||
|
} |
||||
|
|
||||
|
// MarshalLogObject implements the zapcore.ObjectMarshaler interface.
|
||||
|
func (mom logObjectMarshalerWrapper) MarshalLogObject(_ zapcore.ObjectEncoder) error { |
||||
|
return mom.marsh.MarshalLogObject(mom.enc) |
||||
|
} |
||||
|
|
||||
|
// Interface guards
|
||||
|
var ( |
||||
|
_ zapcore.Encoder = (*FilterEncoder)(nil) |
||||
|
_ zapcore.ObjectMarshaler = (*logObjectMarshalerWrapper)(nil) |
||||
|
) |
@ -0,0 +1,94 @@ |
|||||
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
|
//
|
||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
// you may not use this file except in compliance with the License.
|
||||
|
// You may obtain a copy of the License at
|
||||
|
//
|
||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
//
|
||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
// See the License for the specific language governing permissions and
|
||||
|
// limitations under the License.
|
||||
|
|
||||
|
package logging |
||||
|
|
||||
|
import ( |
||||
|
"net" |
||||
|
|
||||
|
"github.com/caddyserver/caddy/v2" |
||||
|
"go.uber.org/zap/zapcore" |
||||
|
) |
||||
|
|
||||
|
func init() { |
||||
|
caddy.RegisterModule(DeleteFilter{}) |
||||
|
caddy.RegisterModule(IPMaskFilter{}) |
||||
|
} |
||||
|
|
||||
|
// LogFieldFilter can filter (or manipulate)
|
||||
|
// a field in a log entry. If delete is true,
|
||||
|
// out will be ignored and the field will be
|
||||
|
// removed from the output.
|
||||
|
type LogFieldFilter interface { |
||||
|
Filter(zapcore.Field) zapcore.Field |
||||
|
} |
||||
|
|
||||
|
// DeleteFilter is a Caddy log field filter that
|
||||
|
// deletes the field.
|
||||
|
type DeleteFilter struct{} |
||||
|
|
||||
|
// CaddyModule returns the Caddy module information.
|
||||
|
func (DeleteFilter) CaddyModule() caddy.ModuleInfo { |
||||
|
return caddy.ModuleInfo{ |
||||
|
Name: "caddy.logging.encoders.filter.delete", |
||||
|
New: func() caddy.Module { return new(DeleteFilter) }, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Filter filters the input field.
|
||||
|
func (DeleteFilter) Filter(in zapcore.Field) zapcore.Field { |
||||
|
in.Type = zapcore.SkipType |
||||
|
return in |
||||
|
} |
||||
|
|
||||
|
// IPMaskFilter is a Caddy log field filter that
|
||||
|
// masks IP addresses.
|
||||
|
type IPMaskFilter struct { |
||||
|
IPv4CIDR int `json:"ipv4_cidr,omitempty"` |
||||
|
IPv6CIDR int `json:"ipv6_cidr,omitempty"` |
||||
|
} |
||||
|
|
||||
|
// CaddyModule returns the Caddy module information.
|
||||
|
func (IPMaskFilter) CaddyModule() caddy.ModuleInfo { |
||||
|
return caddy.ModuleInfo{ |
||||
|
Name: "caddy.logging.encoders.filter.ip_mask", |
||||
|
New: func() caddy.Module { return new(IPMaskFilter) }, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Filter filters the input field.
|
||||
|
func (m IPMaskFilter) Filter(in zapcore.Field) zapcore.Field { |
||||
|
host, port, err := net.SplitHostPort(in.String) |
||||
|
if err != nil { |
||||
|
host = in.String // assume whole thing was IP address
|
||||
|
} |
||||
|
ipAddr := net.ParseIP(host) |
||||
|
if ipAddr == nil { |
||||
|
return in |
||||
|
} |
||||
|
bitLen := 32 |
||||
|
cidrPrefix := m.IPv4CIDR |
||||
|
if ipAddr.To16() != nil { |
||||
|
bitLen = 128 |
||||
|
cidrPrefix = m.IPv6CIDR |
||||
|
} |
||||
|
mask := net.CIDRMask(cidrPrefix, bitLen) |
||||
|
masked := ipAddr.Mask(mask) |
||||
|
if port == "" { |
||||
|
in.String = masked.String() |
||||
|
} else { |
||||
|
in.String = net.JoinHostPort(masked.String(), port) |
||||
|
} |
||||
|
return in |
||||
|
} |
@ -0,0 +1,114 @@ |
|||||
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
|
//
|
||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
// you may not use this file except in compliance with the License.
|
||||
|
// You may obtain a copy of the License at
|
||||
|
//
|
||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
//
|
||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
// See the License for the specific language governing permissions and
|
||||
|
// limitations under the License.
|
||||
|
|
||||
|
package logging |
||||
|
|
||||
|
import ( |
||||
|
"time" |
||||
|
|
||||
|
"go.uber.org/zap/buffer" |
||||
|
"go.uber.org/zap/zapcore" |
||||
|
) |
||||
|
|
||||
|
// nopEncoder is a zapcore.Encoder that does nothing.
|
||||
|
type nopEncoder struct{} |
||||
|
|
||||
|
// AddArray is part of the zapcore.ObjectEncoder interface.
|
||||
|
// Array elements do not get filtered.
|
||||
|
func (nopEncoder) AddArray(key string, marshaler zapcore.ArrayMarshaler) error { return nil } |
||||
|
|
||||
|
// AddObject is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (nopEncoder) AddObject(key string, marshaler zapcore.ObjectMarshaler) error { return nil } |
||||
|
|
||||
|
// AddBinary is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (nopEncoder) AddBinary(key string, value []byte) {} |
||||
|
|
||||
|
// AddByteString is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (nopEncoder) AddByteString(key string, value []byte) {} |
||||
|
|
||||
|
// AddBool is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (nopEncoder) AddBool(key string, value bool) {} |
||||
|
|
||||
|
// AddComplex128 is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (nopEncoder) AddComplex128(key string, value complex128) {} |
||||
|
|
||||
|
// AddComplex64 is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (nopEncoder) AddComplex64(key string, value complex64) {} |
||||
|
|
||||
|
// AddDuration is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (nopEncoder) AddDuration(key string, value time.Duration) {} |
||||
|
|
||||
|
// AddFloat64 is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (nopEncoder) AddFloat64(key string, value float64) {} |
||||
|
|
||||
|
// AddFloat32 is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (nopEncoder) AddFloat32(key string, value float32) {} |
||||
|
|
||||
|
// AddInt is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (nopEncoder) AddInt(key string, value int) {} |
||||
|
|
||||
|
// AddInt64 is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (nopEncoder) AddInt64(key string, value int64) {} |
||||
|
|
||||
|
// AddInt32 is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (nopEncoder) AddInt32(key string, value int32) {} |
||||
|
|
||||
|
// AddInt16 is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (nopEncoder) AddInt16(key string, value int16) {} |
||||
|
|
||||
|
// AddInt8 is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (nopEncoder) AddInt8(key string, value int8) {} |
||||
|
|
||||
|
// AddString is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (nopEncoder) AddString(key, value string) {} |
||||
|
|
||||
|
// AddTime is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (nopEncoder) AddTime(key string, value time.Time) {} |
||||
|
|
||||
|
// AddUint is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (nopEncoder) AddUint(key string, value uint) {} |
||||
|
|
||||
|
// AddUint64 is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (nopEncoder) AddUint64(key string, value uint64) {} |
||||
|
|
||||
|
// AddUint32 is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (nopEncoder) AddUint32(key string, value uint32) {} |
||||
|
|
||||
|
// AddUint16 is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (nopEncoder) AddUint16(key string, value uint16) {} |
||||
|
|
||||
|
// AddUint8 is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (nopEncoder) AddUint8(key string, value uint8) {} |
||||
|
|
||||
|
// AddUintptr is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (nopEncoder) AddUintptr(key string, value uintptr) {} |
||||
|
|
||||
|
// AddReflected is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (nopEncoder) AddReflected(key string, value interface{}) error { return nil } |
||||
|
|
||||
|
// OpenNamespace is part of the zapcore.ObjectEncoder interface.
|
||||
|
func (nopEncoder) OpenNamespace(key string) {} |
||||
|
|
||||
|
// Clone is part of the zapcore.ObjectEncoder interface.
|
||||
|
// We don't use it as of Oct 2019 (v2 beta 7), I'm not
|
||||
|
// really sure what it'd be useful for in our case.
|
||||
|
func (ne nopEncoder) Clone() zapcore.Encoder { return ne } |
||||
|
|
||||
|
// EncodeEntry partially implements the zapcore.Encoder interface.
|
||||
|
func (nopEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) { |
||||
|
return bufferpool.Get(), nil |
||||
|
} |
||||
|
|
||||
|
// Interface guard
|
||||
|
var _ zapcore.Encoder = (*nopEncoder)(nil) |
Loading…
Reference in new issue