Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 14, 05:15 UTC
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)
Matt Holt committed
Feb 25, 2020 at 22:00 UTC
5d97522d18da39cd3f9dd309774a5ad2c51f4c51
9 files changed
+428
-21
caddyconfig/httpcaddyfile/builtins.go
+110
@@ -24,8 +24,10 @@ import (
24
25
"github.com/caddyserver/caddy/v2"
26
"github.com/caddyserver/caddy/v2/caddyconfig"
27
+ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
28
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
29
"github.com/caddyserver/caddy/v2/modules/caddytls"
30
+ "go.uber.org/zap/zapcore"
31
)
32
33
func init() {
@@ -37,6 +39,7 @@ func init() {
39
RegisterHandlerDirective("route", parseRoute)
40
RegisterHandlerDirective("handle", parseSegmentAsSubroute)
41
RegisterDirective("handle_errors", parseHandleErrors)
42
+ RegisterDirective("log", parseLog)
43
}
44
45
// parseBind parses the bind directive. Syntax:
@@ -426,9 +429,116 @@ func parseHandleErrors(h Helper) ([]ConfigValue, error) {
429
}, nil
430
}
431
432
+// parseLog parses the log directive. Syntax:
433
+//
434
+// log {
435
+// output <writer_module> ...
436
+// format <encoder_module> ...
437
+// level <level>
438
+// }
439
+//
440
+func parseLog(h Helper) ([]ConfigValue, error) {
441
+ var configValues []ConfigValue
442
+ for h.Next() {
443
+ cl := new(caddy.CustomLog)
444
+
445
+ for h.NextBlock(0) {
446
+ switch h.Val() {
447
+ case "output":
448
+ if !h.NextArg() {
449
+ return nil, h.ArgErr()
450
+ }
451
+ moduleName := h.Val()
452
+
453
+ // can't use the usual caddyfile.Unmarshaler flow with the
454
+ // standard writers because they are in the caddy package
455
+ // (because they are the default) and implementing that
456
+ // interface there would unfortunately create circular import
457
+ var wo caddy.WriterOpener
458
+ switch moduleName {
459
+ case "stdout":
460
+ wo = caddy.StdoutWriter{}
461
+ case "stderr":
462
+ wo = caddy.StderrWriter{}
463
+ case "discard":
464
+ wo = caddy.DiscardWriter{}
465
+ default:
466
+ mod, err := caddy.GetModule("caddy.logging.writers." + moduleName)
467
+ if err != nil {
468
+ return nil, h.Errf("getting log writer module named '%s': %v", moduleName, err)
469
+ }
470
+ unm, ok := mod.New().(caddyfile.Unmarshaler)
471
+ if !ok {
472
+ return nil, h.Errf("log writer module '%s' is not a Caddyfile unmarshaler", mod)
473
+ }
474
+ err = unm.UnmarshalCaddyfile(h.NewFromNextSegment())
475
+ if err != nil {
476
+ return nil, err
477
+ }
478
+ wo, ok = unm.(caddy.WriterOpener)
479
+ if !ok {
480
+ return nil, h.Errf("module %s is not a WriterOpener", mod)
481
+ }
482
+ }
483
+ cl.WriterRaw = caddyconfig.JSONModuleObject(wo, "output", moduleName, h.warnings)
484
+
485
+ case "format":
486
+ if !h.NextArg() {
487
+ return nil, h.ArgErr()
488
+ }
489
+ moduleName := h.Val()
490
+ mod, err := caddy.GetModule("caddy.logging.encoders." + moduleName)
491
+ if err != nil {
492
+ return nil, h.Errf("getting log encoder module named '%s': %v", moduleName, err)
493
+ }
494
+ unm, ok := mod.New().(caddyfile.Unmarshaler)
495
+ if !ok {
496
+ return nil, h.Errf("log encoder module '%s' is not a Caddyfile unmarshaler", mod)
497
+ }
498
+ err = unm.UnmarshalCaddyfile(h.NewFromNextSegment())
499
+ if err != nil {
500
+ return nil, err
501
+ }
502
+ enc, ok := unm.(zapcore.Encoder)
503
+ if !ok {
504
+ return nil, h.Errf("module %s is not a zapcore.Encoder", mod)
505
+ }
506
+ cl.EncoderRaw = caddyconfig.JSONModuleObject(enc, "format", moduleName, h.warnings)
507
+
508
+ case "level":
509
+ if !h.NextArg() {
510
+ return nil, h.ArgErr()
511
+ }
512
+ cl.Level = h.Val()
513
+ if h.NextArg() {
514
+ return nil, h.ArgErr()
515
+ }
516
+
517
+ default:
518
+ return nil, h.Errf("unrecognized subdirective: %s", h.Val())
519
+ }
520
+ }
521
+
522
+ var val namedCustomLog
523
+ if !reflect.DeepEqual(cl, new(caddy.CustomLog)) {
524
+ cl.Include = []string{"http.log.access"}
525
+ val.name = fmt.Sprintf("log%d", logCounter)
526
+ val.log = cl
527
+ logCounter++
528
+ }
529
+ configValues = append(configValues, ConfigValue{
530
+ Class: "custom_log",
531
+ Value: val,
532
+ })
533
+ }
534
+ return configValues, nil
535
+}
536
+
537
// tlsCertTags maps certificate filenames to their tag.
538
// This is used to remember which tag is used for each
539
// certificate files, since we need to avoid loading
540
// the same certificate files more than once, overwriting
541
// previous tags
542
var tlsCertTags = make(map[string]string)
543
+
544
+var logCounter int
caddyconfig/httpcaddyfile/httptype.go
+67
@@ -282,6 +282,35 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock,
282
}
283
}
284
285
+ // extract any custom logs, and enforce configured levels
286
+ var customLogs []namedCustomLog
287
+ var hasDefaultLog bool
288
+ for _, sb := range serverBlocks {
289
+ for _, clVal := range sb.pile["custom_log"] {
290
+ ncl := clVal.Value.(namedCustomLog)
291
+ if ncl.name == "" {
292
+ continue
293
+ }
294
+ if ncl.name == "default" {
295
+ hasDefaultLog = true
296
+ }
297
+ if _, ok := options["debug"]; ok && ncl.log.Level == "" {
298
+ ncl.log.Level = "DEBUG"
299
+ }
300
+ customLogs = append(customLogs, ncl)
301
+ }
302
+ }
303
+ if !hasDefaultLog {
304
+ // if the default log was not customized, ensure we
305
+ // configure it with any applicable options
306
+ if _, ok := options["debug"]; ok {
307
+ customLogs = append(customLogs, namedCustomLog{
308
+ name: "default",
309
+ log: &caddy.CustomLog{Level: "DEBUG"},
310
+ })
311
+ }
312
+ }
313
+
314
// annnd the top-level config, then we're done!
315
cfg := &caddy.Config{AppsRaw: make(caddy.ModuleMap)}
316
if !reflect.DeepEqual(httpApp, caddyhttp.App{}) {
@@ -299,6 +328,18 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock,
328
if adminConfig, ok := options["admin"].(string); ok && adminConfig != "" {
329
cfg.Admin = &caddy.AdminConfig{Listen: adminConfig}
330
}
331
+ if len(customLogs) > 0 {
332
+ if cfg.Logging == nil {
333
+ cfg.Logging = &caddy.Logging{
334
+ Logs: make(map[string]*caddy.CustomLog),
335
+ }
336
+ }
337
+ for _, ncl := range customLogs {
338
+ if ncl.name != "" {
339
+ cfg.Logging.Logs[ncl.name] = ncl.log
340
+ }
341
+ }
342
+ }
343
344
return cfg, warnings, nil
345
}
@@ -335,6 +376,8 @@ func (ServerType) evaluateGlobalOptionsBlock(serverBlocks []serverBlock, options
376
val, err = parseOptEmail(disp)
377
case "admin":
378
val, err = parseOptAdmin(disp)
379
+ case "debug":
380
+ options["debug"] = true
381
default:
382
return nil, fmt.Errorf("unrecognized parameter name: %s", dir)
383
}
@@ -506,6 +549,25 @@ func (st *ServerType) serversFromPairings(
549
srv.Errors.Routes = appendSubrouteToRouteList(srv.Errors.Routes, sr, matcherSetsEnc, p, warnings)
550
}
551
}
552
+
553
+ // add log associations
554
+ for _, cval := range sblock.pile["custom_log"] {
555
+ ncl := cval.Value.(namedCustomLog)
556
+ if srv.Logs == nil {
557
+ srv.Logs = &caddyhttp.ServerLogConfig{
558
+ LoggerNames: make(map[string]string),
559
+ }
560
+ }
561
+ hosts, err := st.hostsFromServerBlockKeys(sblock.block)
562
+ if err != nil {
563
+ return nil, err
564
+ }
565
+ for _, h := range hosts {
566
+ if ncl.name != "" {
567
+ srv.Logs.LoggerNames[h] = ncl.name
568
+ }
569
+ }
570
+ }
571
}
572
573
// a catch-all TLS conn policy is necessary to ensure TLS can
@@ -940,6 +1002,11 @@ type matcherSetAndTokens struct {
1002
tokens []caddyfile.Token
1003
}
1004
1005
+type namedCustomLog struct {
1006
+ name string
1007
+ log *caddy.CustomLog
1008
+}
1009
+
1010
// sbAddrAssocation is a mapping from a list of
1011
// addresses to a list of server blocks that are
1012
// served on those addresses.
go.mod
+1
-1
@@ -7,7 +7,7 @@ require (
7
github.com/alecthomas/chroma v0.7.0
8
github.com/andybalholm/brotli v0.0.0-20190821151343-b60f0d972eeb
9
github.com/cenkalti/backoff/v3 v3.1.1 // indirect
10
- github.com/dustin/go-humanize v1.0.0
10
+ github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac
11
github.com/go-acme/lego/v3 v3.3.0
12
github.com/golang/groupcache v0.0.0-20191002201903-404acd9df4cc
13
github.com/ilibs/json5 v1.0.1
go.sum
+2
@@ -94,6 +94,8 @@ github.com/dnaeon/go-vcr v0.0.0-20180814043457-aafff18a5cc2/go.mod h1:aBB1+wY4s9
94
github.com/dnsimple/dnsimple-go v0.30.0/go.mod h1:O5TJ0/U6r7AfT8niYNlmohpLbCSG+c71tQlGr9SeGrg=
95
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
96
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
97
+github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac h1:opbrjaN/L8gg6Xh5D04Tem+8xVcz6ajZlGCs49mQgyg=
98
+github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
99
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
100
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
101
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
modules/logging/encoders.go
+143
-12
@@ -21,6 +21,7 @@ import (
21
"time"
22
23
"github.com/caddyserver/caddy/v2"
24
+ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
25
zaplogfmt "github.com/jsternberg/zap-logfmt"
26
"go.uber.org/zap"
27
"go.uber.org/zap/buffer"
@@ -31,7 +32,7 @@ func init() {
32
caddy.RegisterModule(ConsoleEncoder{})
33
caddy.RegisterModule(JSONEncoder{})
34
caddy.RegisterModule(LogfmtEncoder{})
34
- caddy.RegisterModule(StringEncoder{})
35
+ caddy.RegisterModule(SingleFieldEncoder{})
36
}
37
38
// ConsoleEncoder encodes log entries that are mostly human-readable.
@@ -54,6 +55,27 @@ func (ce *ConsoleEncoder) Provision(_ caddy.Context) error {
55
return nil
56
}
57
58
+// UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax:
59
+//
60
+// console {
61
+// <common encoder config subdirectives...>
62
+// }
63
+//
64
+// See the godoc on the LogEncoderConfig type for the syntax of
65
+// subdirectives that are common to most/all encoders.
66
+func (ce *ConsoleEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
67
+ for d.Next() {
68
+ if d.NextArg() {
69
+ return d.ArgErr()
70
+ }
71
+ err := ce.LogEncoderConfig.UnmarshalCaddyfile(d)
72
+ if err != nil {
73
+ return err
74
+ }
75
+ }
76
+ return nil
77
+}
78
+
79
// JSONEncoder encodes entries as JSON.
80
type JSONEncoder struct {
81
zapcore.Encoder `json:"-"`
@@ -74,6 +96,27 @@ func (je *JSONEncoder) Provision(_ caddy.Context) error {
96
return nil
97
}
98
99
+// UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax:
100
+//
101
+// json {
102
+// <common encoder config subdirectives...>
103
+// }
104
+//
105
+// See the godoc on the LogEncoderConfig type for the syntax of
106
+// subdirectives that are common to most/all encoders.
107
+func (je *JSONEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
108
+ for d.Next() {
109
+ if d.NextArg() {
110
+ return d.ArgErr()
111
+ }
112
+ err := je.LogEncoderConfig.UnmarshalCaddyfile(d)
113
+ if err != nil {
114
+ return err
115
+ }
116
+ }
117
+ return nil
118
+}
119
+
120
// LogfmtEncoder encodes log entries as logfmt:
121
// https://www.brandur.org/logfmt
122
type LogfmtEncoder struct {
@@ -95,26 +138,47 @@ func (lfe *LogfmtEncoder) Provision(_ caddy.Context) error {
138
return nil
139
}
140
98
-// StringEncoder writes a log entry that consists entirely
141
+// UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax:
142
+//
143
+// logfmt {
144
+// <common encoder config subdirectives...>
145
+// }
146
+//
147
+// See the godoc on the LogEncoderConfig type for the syntax of
148
+// subdirectives that are common to most/all encoders.
149
+func (lfe *LogfmtEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
150
+ for d.Next() {
151
+ if d.NextArg() {
152
+ return d.ArgErr()
153
+ }
154
+ err := lfe.LogEncoderConfig.UnmarshalCaddyfile(d)
155
+ if err != nil {
156
+ return err
157
+ }
158
+ }
159
+ return nil
160
+}
161
+
162
+// SingleFieldEncoder writes a log entry that consists entirely
163
// of a single string field in the log entry. This is useful
164
// for custom, self-encoded log entries that consist of a
165
// single field in the structured log entry.
102
-type StringEncoder struct {
166
+type SingleFieldEncoder struct {
167
zapcore.Encoder `json:"-"`
168
FieldName string `json:"field,omitempty"`
169
FallbackRaw json.RawMessage `json:"fallback,omitempty" caddy:"namespace=caddy.logging.encoders inline_key=format"`
170
}
171
172
// CaddyModule returns the Caddy module information.
109
-func (StringEncoder) CaddyModule() caddy.ModuleInfo {
173
+func (SingleFieldEncoder) CaddyModule() caddy.ModuleInfo {
174
return caddy.ModuleInfo{
111
- ID: "caddy.logging.encoders.string",
112
- New: func() caddy.Module { return new(StringEncoder) },
175
+ ID: "caddy.logging.encoders.single_field",
176
+ New: func() caddy.Module { return new(SingleFieldEncoder) },
177
}
178
}
179
180
// Provision sets up the encoder.
117
-func (se *StringEncoder) Provision(ctx caddy.Context) error {
181
+func (se *SingleFieldEncoder) Provision(ctx caddy.Context) error {
182
if se.FallbackRaw != nil {
183
val, err := ctx.LoadModule(se, "FallbackRaw")
184
if err != nil {
@@ -132,16 +196,16 @@ func (se *StringEncoder) Provision(ctx caddy.Context) error {
196
// necessary because we implement our own EncodeEntry,
197
// and if we simply let the embedded encoder's Clone
198
// be promoted, it would return a clone of that, and
135
-// we'd lose our StringEncoder's EncodeEntry.
136
-func (se StringEncoder) Clone() zapcore.Encoder {
137
- return StringEncoder{
199
+// we'd lose our SingleFieldEncoder's EncodeEntry.
200
+func (se SingleFieldEncoder) Clone() zapcore.Encoder {
201
+ return SingleFieldEncoder{
202
Encoder: se.Encoder.Clone(),
203
FieldName: se.FieldName,
204
}
205
}
206
207
// EncodeEntry partially implements the zapcore.Encoder interface.
144
-func (se StringEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
208
+func (se SingleFieldEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
209
for _, f := range fields {
210
if f.Key == se.FieldName {
211
buf := bufferpool.Get()
@@ -158,6 +222,21 @@ func (se StringEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (
222
return se.Encoder.EncodeEntry(ent, fields)
223
}
224
225
+// UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax:
226
+//
227
+// single_field <field_name>
228
+//
229
+func (se *SingleFieldEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
230
+ for d.Next() {
231
+ var fieldName string
232
+ if !d.AllArgs(&fieldName) {
233
+ return d.ArgErr()
234
+ }
235
+ se.FieldName = d.Val()
236
+ }
237
+ return nil
238
+}
239
+
240
// LogEncoderConfig holds configuration common to most encoders.
241
type LogEncoderConfig struct {
242
MessageKey *string `json:"message_key,omitempty"`
@@ -172,6 +251,53 @@ type LogEncoderConfig struct {
251
LevelFormat string `json:"level_format,omitempty"`
252
}
253
254
+// UnmarshalCaddyfile populates the struct from Caddyfile tokens. Syntax:
255
+//
256
+// {
257
+// message_key <key>
258
+// level_key <key>
259
+// time_key <key>
260
+// name_key <key>
261
+// caller_key <key>
262
+// stacktrace_key <key>
263
+// line_ending <char>
264
+// time_format <format>
265
+// level_format <format>
266
+// }
267
+//
268
+func (lec *LogEncoderConfig) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
269
+ for nesting := d.Nesting(); d.NextBlock(nesting); {
270
+ subdir := d.Val()
271
+ var arg string
272
+ if !d.AllArgs(&arg) {
273
+ return d.ArgErr()
274
+ }
275
+ switch subdir {
276
+ case "message_key":
277
+ lec.MessageKey = &arg
278
+ case "level_key":
279
+ lec.LevelKey = &arg
280
+ case "time_key":
281
+ lec.TimeKey = &arg
282
+ case "name_key":
283
+ lec.NameKey = &arg
284
+ case "caller_key":
285
+ lec.CallerKey = &arg
286
+ case "stacktrace_key":
287
+ lec.StacktraceKey = &arg
288
+ case "line_ending":
289
+ lec.LineEnding = &arg
290
+ case "time_format":
291
+ lec.TimeFormat = arg
292
+ case "level_format":
293
+ lec.LevelFormat = arg
294
+ default:
295
+ return d.Errf("unrecognized subdirective %s", subdir)
296
+ }
297
+ }
298
+ return nil
299
+}
300
+
301
// ZapcoreEncoderConfig returns the equivalent zapcore.EncoderConfig.
302
// If lec is nil, zap.NewProductionEncoderConfig() is returned.
303
func (lec *LogEncoderConfig) ZapcoreEncoderConfig() zapcore.EncoderConfig {
@@ -263,5 +389,10 @@ var (
389
_ zapcore.Encoder = (*ConsoleEncoder)(nil)
390
_ zapcore.Encoder = (*JSONEncoder)(nil)
391
_ zapcore.Encoder = (*LogfmtEncoder)(nil)
266
- _ zapcore.Encoder = (*StringEncoder)(nil)
392
+ _ zapcore.Encoder = (*SingleFieldEncoder)(nil)
393
+
394
+ _ caddyfile.Unmarshaler = (*ConsoleEncoder)(nil)
395
+ _ caddyfile.Unmarshaler = (*JSONEncoder)(nil)
396
+ _ caddyfile.Unmarshaler = (*LogfmtEncoder)(nil)
397
+ _ caddyfile.Unmarshaler = (*SingleFieldEncoder)(nil)
398
)
modules/logging/filewriter.go
+75
-1
@@ -19,8 +19,12 @@ import (
19
"io"
20
"os"
21
"path/filepath"
22
+ "strconv"
23
+ "time"
24
25
"github.com/caddyserver/caddy/v2"
26
+ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
27
+ "github.com/dustin/go-humanize"
28
"gopkg.in/natefinch/lumberjack.v2"
29
)
30
@@ -125,7 +129,77 @@ func (fw FileWriter) OpenWriter() (io.WriteCloser, error) {
129
return os.OpenFile(fw.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
130
}
131
132
+// UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax:
133
+//
134
+// file <filename> {
135
+// roll_disabled
136
+// roll_size <size>
137
+// roll_keep <num>
138
+// roll_keep_for <days>
139
+// }
140
+//
141
+// The roll_size value will be rounded down to number of megabytes (MiB).
142
+// The roll_keep_for duration will be rounded down to number of days.
143
+func (fw *FileWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
144
+ for d.Next() {
145
+ if !d.NextArg() {
146
+ return d.ArgErr()
147
+ }
148
+ fw.Filename = d.Val()
149
+ if d.NextArg() {
150
+ return d.ArgErr()
151
+ }
152
+
153
+ for d.NextBlock(0) {
154
+ switch d.Val() {
155
+ case "roll_disabled":
156
+ var f bool
157
+ fw.Roll = &f
158
+ if d.NextArg() {
159
+ return d.ArgErr()
160
+ }
161
+
162
+ case "roll_size":
163
+ var sizeStr string
164
+ if !d.AllArgs(&sizeStr) {
165
+ return d.ArgErr()
166
+ }
167
+ size, err := humanize.ParseBytes(sizeStr)
168
+ if err != nil {
169
+ return d.Errf("parsing size: %v", err)
170
+ }
171
+ fw.RollSizeMB = int(size) / 1024 / 1024
172
+
173
+ case "roll_keep":
174
+ var keepStr string
175
+ if !d.AllArgs(&keepStr) {
176
+ return d.ArgErr()
177
+ }
178
+ keep, err := strconv.Atoi(keepStr)
179
+ if err != nil {
180
+ return d.Errf("parsing roll_keep number: %v", err)
181
+ }
182
+ fw.RollKeep = keep
183
+
184
+ case "roll_keep_for":
185
+ var keepForStr string
186
+ if !d.AllArgs(&keepForStr) {
187
+ return d.ArgErr()
188
+ }
189
+ keepFor, err := time.ParseDuration(keepForStr)
190
+ if err != nil {
191
+ return d.Errf("parsing roll_keep_for duration: %v", err)
192
+ }
193
+ fw.RollKeepDays = int(keepFor.Hours()) / 24
194
+ }
195
+ }
196
+ }
197
+ return nil
198
+}
199
+
200
// Interface guards
201
var (
130
- _ caddy.Provisioner = (*FileWriter)(nil)
202
+ _ caddy.Provisioner = (*FileWriter)(nil)
203
+ _ caddy.WriterOpener = (*FileWriter)(nil)
204
+ _ caddyfile.Unmarshaler = (*FileWriter)(nil)
205
)
modules/logging/netwriter.go
+21
-2
@@ -20,6 +20,7 @@ import (
20
"net"
21
22
"github.com/caddyserver/caddy/v2"
23
+ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
24
)
25
26
func init() {
@@ -75,8 +76,26 @@ func (nw NetWriter) OpenWriter() (io.WriteCloser, error) {
76
return net.Dial(nw.addr.Network, nw.addr.JoinHostPort(0))
77
}
78
79
+// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax:
80
+//
81
+// net <address>
82
+//
83
+func (nw *NetWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
84
+ for d.Next() {
85
+ if !d.NextArg() {
86
+ return d.ArgErr()
87
+ }
88
+ nw.Address = d.Val()
89
+ if d.NextArg() {
90
+ return d.ArgErr()
91
+ }
92
+ }
93
+ return nil
94
+}
95
+
96
// Interface guards
97
var (
80
- _ caddy.Provisioner = (*NetWriter)(nil)
81
- _ caddy.WriterOpener = (*NetWriter)(nil)
98
+ _ caddy.Provisioner = (*NetWriter)(nil)
99
+ _ caddy.WriterOpener = (*NetWriter)(nil)
100
+ _ caddyfile.Unmarshaler = (*NetWriter)(nil)
101
)
replacer.go
+4
-5
@@ -148,11 +148,10 @@ func (r *Replacer) replace(input, empty string,
148
if errOnUnknown {
149
return "", fmt.Errorf("unrecognized placeholder %s%s%s",
150
string(phOpen), key, string(phClose))
151
- } else if treatUnknownAsEmpty {
152
- if empty != "" {
153
- sb.WriteString(empty)
154
- }
155
- } else {
151
+ } else if !treatUnknownAsEmpty {
152
+ // if treatUnknownAsEmpty is true, we'll
153
+ // handle an empty val later; so only
154
+ // continue otherwise
155
lastWriteCursor = i
156
continue
157
}
replacer_test.go
+5
@@ -67,6 +67,11 @@ func TestReplacer(t *testing.T) {
67
input: `{{}`,
68
expect: "",
69
},
70
+ {
71
+ input: `{unknown}`,
72
+ empty: "-",
73
+ expect: "-",
74
+ },
75
} {
76
actual := rep.ReplaceAll(tc.input, tc.empty)
77
if actual != tc.expect {