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