From 5d97522d18da39cd3f9dd309774a5ad2c51f4c51 Mon Sep 17 00:00:00 2001 From: Matt Holt Date: Tue, 25 Feb 2020 22:00:33 -0700 Subject: v2: 'log' directive for Caddyfile, and debug mode (#3052) * httpcaddyfile: Begin implementing log directive, and debug mode For now, debug mode just sets the log level for all logs to DEBUG (unless a level is specified explicitly). * httpcaddyfile: Finish 'log' directive Also rename StringEncoder -> SingleFieldEncoder * Fix minor bug in replacer (when vals are empty) --- modules/logging/encoders.go | 155 ++++++++++++++++++++++++++++++++++++++---- modules/logging/filewriter.go | 76 ++++++++++++++++++++- modules/logging/netwriter.go | 23 ++++++- 3 files changed, 239 insertions(+), 15 deletions(-) (limited to 'modules/logging') diff --git a/modules/logging/encoders.go b/modules/logging/encoders.go index 49ad11a..bd120d5 100644 --- a/modules/logging/encoders.go +++ b/modules/logging/encoders.go @@ -21,6 +21,7 @@ import ( "time" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" zaplogfmt "github.com/jsternberg/zap-logfmt" "go.uber.org/zap" "go.uber.org/zap/buffer" @@ -31,7 +32,7 @@ func init() { caddy.RegisterModule(ConsoleEncoder{}) caddy.RegisterModule(JSONEncoder{}) caddy.RegisterModule(LogfmtEncoder{}) - caddy.RegisterModule(StringEncoder{}) + caddy.RegisterModule(SingleFieldEncoder{}) } // ConsoleEncoder encodes log entries that are mostly human-readable. @@ -54,6 +55,27 @@ func (ce *ConsoleEncoder) Provision(_ caddy.Context) error { return nil } +// UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax: +// +// console { +// +// } +// +// See the godoc on the LogEncoderConfig type for the syntax of +// subdirectives that are common to most/all encoders. +func (ce *ConsoleEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + if d.NextArg() { + return d.ArgErr() + } + err := ce.LogEncoderConfig.UnmarshalCaddyfile(d) + if err != nil { + return err + } + } + return nil +} + // JSONEncoder encodes entries as JSON. type JSONEncoder struct { zapcore.Encoder `json:"-"` @@ -74,6 +96,27 @@ func (je *JSONEncoder) Provision(_ caddy.Context) error { return nil } +// UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax: +// +// json { +// +// } +// +// See the godoc on the LogEncoderConfig type for the syntax of +// subdirectives that are common to most/all encoders. +func (je *JSONEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + if d.NextArg() { + return d.ArgErr() + } + err := je.LogEncoderConfig.UnmarshalCaddyfile(d) + if err != nil { + return err + } + } + return nil +} + // LogfmtEncoder encodes log entries as logfmt: // https://www.brandur.org/logfmt type LogfmtEncoder struct { @@ -95,26 +138,47 @@ func (lfe *LogfmtEncoder) Provision(_ caddy.Context) error { return nil } -// StringEncoder writes a log entry that consists entirely +// UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax: +// +// logfmt { +// +// } +// +// See the godoc on the LogEncoderConfig type for the syntax of +// subdirectives that are common to most/all encoders. +func (lfe *LogfmtEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + if d.NextArg() { + return d.ArgErr() + } + err := lfe.LogEncoderConfig.UnmarshalCaddyfile(d) + if err != nil { + return err + } + } + return nil +} + +// SingleFieldEncoder 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 { +type SingleFieldEncoder struct { zapcore.Encoder `json:"-"` FieldName string `json:"field,omitempty"` FallbackRaw json.RawMessage `json:"fallback,omitempty" caddy:"namespace=caddy.logging.encoders inline_key=format"` } // CaddyModule returns the Caddy module information. -func (StringEncoder) CaddyModule() caddy.ModuleInfo { +func (SingleFieldEncoder) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - ID: "caddy.logging.encoders.string", - New: func() caddy.Module { return new(StringEncoder) }, + ID: "caddy.logging.encoders.single_field", + New: func() caddy.Module { return new(SingleFieldEncoder) }, } } // Provision sets up the encoder. -func (se *StringEncoder) Provision(ctx caddy.Context) error { +func (se *SingleFieldEncoder) Provision(ctx caddy.Context) error { if se.FallbackRaw != nil { val, err := ctx.LoadModule(se, "FallbackRaw") if err != nil { @@ -132,16 +196,16 @@ func (se *StringEncoder) Provision(ctx caddy.Context) error { // 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{ +// we'd lose our SingleFieldEncoder's EncodeEntry. +func (se SingleFieldEncoder) Clone() zapcore.Encoder { + return SingleFieldEncoder{ 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) { +func (se SingleFieldEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) { for _, f := range fields { if f.Key == se.FieldName { buf := bufferpool.Get() @@ -158,6 +222,21 @@ func (se StringEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) ( return se.Encoder.EncodeEntry(ent, fields) } +// UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax: +// +// single_field +// +func (se *SingleFieldEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + var fieldName string + if !d.AllArgs(&fieldName) { + return d.ArgErr() + } + se.FieldName = d.Val() + } + return nil +} + // LogEncoderConfig holds configuration common to most encoders. type LogEncoderConfig struct { MessageKey *string `json:"message_key,omitempty"` @@ -172,6 +251,53 @@ type LogEncoderConfig struct { LevelFormat string `json:"level_format,omitempty"` } +// UnmarshalCaddyfile populates the struct from Caddyfile tokens. Syntax: +// +// { +// message_key +// level_key +// time_key +// name_key +// caller_key +// stacktrace_key +// line_ending +// time_format +// level_format +// } +// +func (lec *LogEncoderConfig) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for nesting := d.Nesting(); d.NextBlock(nesting); { + subdir := d.Val() + var arg string + if !d.AllArgs(&arg) { + return d.ArgErr() + } + switch subdir { + case "message_key": + lec.MessageKey = &arg + case "level_key": + lec.LevelKey = &arg + case "time_key": + lec.TimeKey = &arg + case "name_key": + lec.NameKey = &arg + case "caller_key": + lec.CallerKey = &arg + case "stacktrace_key": + lec.StacktraceKey = &arg + case "line_ending": + lec.LineEnding = &arg + case "time_format": + lec.TimeFormat = arg + case "level_format": + lec.LevelFormat = arg + default: + return d.Errf("unrecognized subdirective %s", subdir) + } + } + return nil +} + // ZapcoreEncoderConfig returns the equivalent zapcore.EncoderConfig. // If lec is nil, zap.NewProductionEncoderConfig() is returned. func (lec *LogEncoderConfig) ZapcoreEncoderConfig() zapcore.EncoderConfig { @@ -263,5 +389,10 @@ var ( _ zapcore.Encoder = (*ConsoleEncoder)(nil) _ zapcore.Encoder = (*JSONEncoder)(nil) _ zapcore.Encoder = (*LogfmtEncoder)(nil) - _ zapcore.Encoder = (*StringEncoder)(nil) + _ zapcore.Encoder = (*SingleFieldEncoder)(nil) + + _ caddyfile.Unmarshaler = (*ConsoleEncoder)(nil) + _ caddyfile.Unmarshaler = (*JSONEncoder)(nil) + _ caddyfile.Unmarshaler = (*LogfmtEncoder)(nil) + _ caddyfile.Unmarshaler = (*SingleFieldEncoder)(nil) ) diff --git a/modules/logging/filewriter.go b/modules/logging/filewriter.go index f17f975..e9c2dd8 100644 --- a/modules/logging/filewriter.go +++ b/modules/logging/filewriter.go @@ -19,8 +19,12 @@ import ( "io" "os" "path/filepath" + "strconv" + "time" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/dustin/go-humanize" "gopkg.in/natefinch/lumberjack.v2" ) @@ -125,7 +129,77 @@ func (fw FileWriter) OpenWriter() (io.WriteCloser, error) { return os.OpenFile(fw.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666) } +// UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax: +// +// file { +// roll_disabled +// roll_size +// roll_keep +// roll_keep_for +// } +// +// The roll_size value will be rounded down to number of megabytes (MiB). +// The roll_keep_for duration will be rounded down to number of days. +func (fw *FileWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + if !d.NextArg() { + return d.ArgErr() + } + fw.Filename = d.Val() + if d.NextArg() { + return d.ArgErr() + } + + for d.NextBlock(0) { + switch d.Val() { + case "roll_disabled": + var f bool + fw.Roll = &f + if d.NextArg() { + return d.ArgErr() + } + + case "roll_size": + var sizeStr string + if !d.AllArgs(&sizeStr) { + return d.ArgErr() + } + size, err := humanize.ParseBytes(sizeStr) + if err != nil { + return d.Errf("parsing size: %v", err) + } + fw.RollSizeMB = int(size) / 1024 / 1024 + + case "roll_keep": + var keepStr string + if !d.AllArgs(&keepStr) { + return d.ArgErr() + } + keep, err := strconv.Atoi(keepStr) + if err != nil { + return d.Errf("parsing roll_keep number: %v", err) + } + fw.RollKeep = keep + + case "roll_keep_for": + var keepForStr string + if !d.AllArgs(&keepForStr) { + return d.ArgErr() + } + keepFor, err := time.ParseDuration(keepForStr) + if err != nil { + return d.Errf("parsing roll_keep_for duration: %v", err) + } + fw.RollKeepDays = int(keepFor.Hours()) / 24 + } + } + } + return nil +} + // Interface guards var ( - _ caddy.Provisioner = (*FileWriter)(nil) + _ caddy.Provisioner = (*FileWriter)(nil) + _ caddy.WriterOpener = (*FileWriter)(nil) + _ caddyfile.Unmarshaler = (*FileWriter)(nil) ) diff --git a/modules/logging/netwriter.go b/modules/logging/netwriter.go index 1df80b6..7d2dafa 100644 --- a/modules/logging/netwriter.go +++ b/modules/logging/netwriter.go @@ -20,6 +20,7 @@ import ( "net" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" ) func init() { @@ -75,8 +76,26 @@ func (nw NetWriter) OpenWriter() (io.WriteCloser, error) { return net.Dial(nw.addr.Network, nw.addr.JoinHostPort(0)) } +// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax: +// +// net
+// +func (nw *NetWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + if !d.NextArg() { + return d.ArgErr() + } + nw.Address = d.Val() + if d.NextArg() { + return d.ArgErr() + } + } + return nil +} + // Interface guards var ( - _ caddy.Provisioner = (*NetWriter)(nil) - _ caddy.WriterOpener = (*NetWriter)(nil) + _ caddy.Provisioner = (*NetWriter)(nil) + _ caddy.WriterOpener = (*NetWriter)(nil) + _ caddyfile.Unmarshaler = (*NetWriter)(nil) ) -- cgit v1.2.3