@marcel91200 / caddy Mirror · read-only / commits / b00dfd39
Fork 0

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: Logging! (#2831)

* 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 conflict

Matt Holt committed Oct 28, 2019 at 14:39 UTC b00dfd3965f400956c5bb5b388e9d54ef98052e5
34 files changed +2234 -201
admin.go
+14 -3
@@ -34,6 +34,7 @@ import (
34 "github.com/caddyserver/caddy/v2/caddyconfig"
35 "github.com/mholt/certmagic"
36 "github.com/rs/cors"
37 + "go.uber.org/zap"
38 )
39
40 var (
@@ -52,6 +53,8 @@ var DefaultAdminConfig = &AdminConfig{
53 Listen: DefaultAdminListen,
54 }
55
56 +// TODO: holy smokes, the admin endpoint might not have to live in caddy's core.
57 +
58 // StartAdmin starts Caddy's administration endpoint,
59 // bootstrapping it with an optional configuration
60 // in the format of JSON bytes. It opens a listener
@@ -113,7 +116,15 @@ func StartAdmin(initialConfigJSON []byte) error {
116 }
117 }
118
116 - handler := cors.Default().Handler(mux)
119 + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
120 + // TODO: improve/organize this logging
121 + Log().Named("admin.request").Info("",
122 + zap.String("method", r.Method),
123 + zap.String("uri", r.RequestURI),
124 + zap.String("remote", r.RemoteAddr),
125 + )
126 + cors.Default().Handler(mux).ServeHTTP(w, r)
127 + })
128
129 cfgEndptSrv = &http.Server{
130 Handler: handler,
@@ -125,14 +136,14 @@ func StartAdmin(initialConfigJSON []byte) error {
136
137 go cfgEndptSrv.Serve(ln)
138
128 - log.Println("Caddy 2 admin endpoint listening on", adminConfig.Listen)
139 + fmt.Println("Caddy 2 admin endpoint listening on", adminConfig.Listen)
140
141 if len(initialConfigJSON) > 0 {
142 err := Load(bytes.NewReader(initialConfigJSON))
143 if err != nil {
144 return fmt.Errorf("loading initial config: %v", err)
145 }
135 - log.Println("Caddy 2 serving initial configuration")
146 + fmt.Println("Caddy 2 serving initial configuration")
147 }
148
149 return nil
caddy.go
+36 -13
@@ -29,16 +29,13 @@ import (
29
30 // Config represents a Caddy configuration.
31 type Config struct {
32 - Admin *AdminConfig `json:"admin,omitempty"`
32 + Admin *AdminConfig `json:"admin,omitempty"`
33 + Logging *Logging `json:"logging,omitempty"`
34 + StorageRaw json.RawMessage `json:"storage,omitempty"`
35 + AppsRaw map[string]json.RawMessage `json:"apps,omitempty"`
36
34 - StorageRaw json.RawMessage `json:"storage,omitempty"`
35 - storage certmagic.Storage
36 -
37 - AppsRaw map[string]json.RawMessage `json:"apps,omitempty"`
38 -
39 - // apps stores the decoded Apps values,
40 - // keyed by module name.
41 - apps map[string]App
37 + apps map[string]App
38 + storage certmagic.Storage
39
40 cancelFunc context.CancelFunc
41 }
@@ -75,7 +72,11 @@ func Run(newCfg *Config) error {
72 // is performed if any modules were provisioned;
73 // apps that were started already will be stopped,
74 // so this function should not leak resources if
78 -// an error is returned.
75 +// an error is returned. However, if no error is
76 +// returned and start == false, you should cancel
77 +// the config if you are not going to start it,
78 +// so that each provisioned module will be
79 +// cleaned up.
80 func run(newCfg *Config, start bool) error {
81 if newCfg == nil {
82 return nil
@@ -102,12 +103,30 @@ func run(newCfg *Config, start bool) error {
103 ctx, cancel := NewContext(Context{Context: context.Background(), cfg: newCfg})
104 defer func() {
105 if err != nil {
105 - cancel() // clean up now
106 + // if there were any errors during startup,
107 + // we should cancel the new context we created
108 + // since the associated config won't be used;
109 + // this will cause all modules that were newly
110 + // provisioned to clean themselves up
111 + cancel()
112 +
113 + // also undo any other state changes we made
114 + if currentCfg != nil {
115 + certmagic.Default.Storage = currentCfg.storage
116 + }
117 }
118 }()
119 newCfg.cancelFunc = cancel // clean up later
120
110 - // set up storage and make it CertMagic's default storage, too
121 + // set up logging before anything bad happens
122 + if newCfg.Logging != nil {
123 + err := newCfg.Logging.openLogs(ctx)
124 + if err != nil {
125 + return err
126 + }
127 + }
128 +
129 + // set up global storage and make it CertMagic's default storage, too
130 err = func() error {
131 if newCfg.StorageRaw != nil {
132 val, err := ctx.LoadModuleInline("module", "caddy.storage", newCfg.StorageRaw)
@@ -214,7 +233,11 @@ func unsyncedStop(cfg *Config) {
233 // Validate loads, provisions, and validates
234 // cfg, but does not start running it.
235 func Validate(cfg *Config) error {
217 - return run(cfg, false)
236 + err := run(cfg, false)
237 + if err == nil {
238 + cfg.cancelFunc() // call Cleanup on all modules
239 + }
240 + return err
241 }
242
243 // Duration is a JSON-string-unmarshable duration type.
cmd/caddy/main.go
+1
@@ -40,6 +40,7 @@ import (
40 _ "github.com/caddyserver/caddy/v2/modules/caddytls"
41 _ "github.com/caddyserver/caddy/v2/modules/caddytls/standardstek"
42 _ "github.com/caddyserver/caddy/v2/modules/filestorage"
43 + _ "github.com/caddyserver/caddy/v2/modules/logging"
44 )
45
46 func main() {
cmd/commandfuncs.go
+2 -2
@@ -387,7 +387,7 @@ func cmdAdaptConfig(fl Flags) (int, error) {
387 if warn.Directive != "" {
388 msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message)
389 }
390 - log.Printf("[WARNING][%s] %s:%d: %s", adaptCmdAdapterFlag, warn.File, warn.Line, msg)
390 + fmt.Fprintf(os.Stderr, "[WARNING][%s] %s:%d: %s\n", adaptCmdAdapterFlag, warn.File, warn.Line, msg)
391 }
392
393 // print result to stdout
@@ -436,7 +436,7 @@ func cmdValidateConfig(fl Flags) (int, error) {
436 if warn.Directive != "" {
437 msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message)
438 }
439 - log.Printf("[WARNING][%s] %s:%d: %s", validateCmdAdapterFlag, warn.File, warn.Line, msg)
439 + fmt.Fprintf(os.Stderr, "[WARNING][%s] %s:%d: %s\n", validateCmdAdapterFlag, warn.File, warn.Line, msg)
440 }
441
442 input = adaptedConfig
cmd/commands.go
+2 -2
@@ -130,9 +130,9 @@ not quit after printing, and can be useful for troubleshooting.`,
130 RegisterCommand(Command{
131 Name: "stop",
132 Func: cmdStop,
133 - Short: "Gracefully stops the running Caddy process",
133 + Short: "Gracefully stops a started Caddy process",
134 Long: `
135 -Stops the running Caddy process as gracefully as possible.
135 +Stops the background Caddy process as gracefully as possible.
136
137 On Windows, this stop is forceful and Caddy will not have an opportunity to
138 clean up any active locks; for a graceful shutdown on Windows, use Ctrl+C
cmd/main.go
+6 -7
@@ -20,7 +20,6 @@ import (
20 "fmt"
21 "io"
22 "io/ioutil"
23 - "log"
23 "net"
24 "os"
25 "strconv"
@@ -38,7 +37,7 @@ func Main() {
37
38 switch len(os.Args) {
39 case 0:
41 - log.Printf("[FATAL] no arguments provided by OS; args[0] must be command")
40 + fmt.Printf("[FATAL] no arguments provided by OS; args[0] must be command\n")
41 os.Exit(caddy.ExitCodeFailedStartup)
42 case 1:
43 os.Args = append(os.Args, "help")
@@ -49,9 +48,9 @@ func Main() {
48 if !ok {
49 if strings.HasPrefix(os.Args[1], "-") {
50 // user probably forgot to type the subcommand
52 - log.Println("[ERROR] first argument must be a subcommand; see 'caddy help'")
51 + fmt.Println("[ERROR] first argument must be a subcommand; see 'caddy help'")
52 } else {
54 - log.Printf("[ERROR] '%s' is not a recognized subcommand; see 'caddy help'", os.Args[1])
53 + fmt.Printf("[ERROR] '%s' is not a recognized subcommand; see 'caddy help'\n", os.Args[1])
54 }
55 os.Exit(caddy.ExitCodeFailedStartup)
56 }
@@ -63,13 +62,13 @@ func Main() {
62
63 err := fs.Parse(os.Args[2:])
64 if err != nil {
66 - log.Println(err)
65 + fmt.Println(err)
66 os.Exit(caddy.ExitCodeFailedStartup)
67 }
68
69 exitCode, err := subcommand.Func(Flags{fs})
70 if err != nil {
72 - log.Printf("%s: %v", subcommand.Name, err)
71 + fmt.Printf("%s: %v\n", subcommand.Name, err)
72 }
73
74 os.Exit(exitCode)
@@ -149,7 +148,7 @@ func loadConfig(configFile, adapterName string) ([]byte, error) {
148 if warn.Directive != "" {
149 msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message)
150 }
152 - fmt.Printf("[WARNING][%s] %s:%d: %s", adapterName, warn.File, warn.Line, msg)
151 + fmt.Printf("[WARNING][%s] %s:%d: %s\n", adapterName, warn.File, warn.Line, msg)
152 }
153 config = adaptedConfig
154 }
cmd/proc_posix.go
+1 -1
@@ -24,7 +24,7 @@ import (
24 )
25
26 func gracefullyStopProcess(pid int) error {
27 - fmt.Printf("Graceful stop...")
27 + fmt.Printf("Graceful stop...\n")
28 err := syscall.Kill(pid, syscall.SIGINT)
29 if err != nil {
30 return fmt.Errorf("kill: %v", err)
cmd/proc_windows.go
+1 -1
@@ -23,7 +23,7 @@ import (
23 )
24
25 func gracefullyStopProcess(pid int) error {
26 - fmt.Printf("Forceful Stop...")
26 + fmt.Printf("Forceful Stop...\n")
27 // process on windows will not stop unless forced with /f
28 cmd := exec.Command("taskkill", "/pid", strconv.Itoa(pid), "/f")
29 if err := cmd.Run(); err != nil {
context.go
+6
@@ -22,6 +22,7 @@ import (
22 "reflect"
23
24 "github.com/mholt/certmagic"
25 + "go.uber.org/zap"
26 )
27
28 // Context is a type which defines the lifetime of modules that
@@ -206,3 +207,8 @@ func (ctx Context) App(name string) (interface{}, error) {
207 func (ctx Context) Storage() certmagic.Storage {
208 return ctx.cfg.storage
209 }
210 +
211 +// Logger returns a logger that can be used by mod.
212 +func (ctx Context) Logger(mod Module) *zap.Logger {
213 + return ctx.cfg.Logging.Logger(mod)
214 +}
go.mod
+6
@@ -11,18 +11,24 @@ require (
11 github.com/golang/groupcache v0.0.0-20191002201903-404acd9df4cc
12 github.com/ilibs/json5 v1.0.1
13 github.com/imdario/mergo v0.3.8 // indirect
14 + github.com/jsternberg/zap-logfmt v1.2.0
15 github.com/keybase/go-ps v0.0.0-20190827175125-91aafc93ba19
16 github.com/klauspost/compress v1.8.6
17 github.com/klauspost/cpuid v1.2.1
18 github.com/lucas-clemente/quic-go v0.12.1
19 github.com/mholt/certmagic v0.8.3
20 github.com/muhammadmuzzammil1998/jsonc v0.0.0-20190906142622-1265e9b150c6
21 + github.com/onsi/ginkgo v1.8.0 // indirect
22 + github.com/onsi/gomega v1.5.0 // indirect
23 github.com/rs/cors v1.7.0
24 github.com/russross/blackfriday/v2 v2.0.1
25 github.com/starlight-go/starlight v0.0.0-20181207205707-b06f321544f3
26 github.com/vulcand/oxy v1.0.0
27 go.starlark.net v0.0.0-20190919145610-979af19b165c
28 + go.uber.org/multierr v1.2.0 // indirect
29 + go.uber.org/zap v1.10.0
30 golang.org/x/crypto v0.0.0-20191010185427-af544f31c8ac
31 golang.org/x/net v0.0.0-20191009170851-d66e71096ffb
32 golang.org/x/time v0.0.0-20190921001708-c4c64cad1fd0
33 + gopkg.in/natefinch/lumberjack.v2 v2.0.0
34 )
go.sum
+18
@@ -15,6 +15,7 @@ github.com/Azure/go-autorest/autorest/to v0.2.0/go.mod h1:GunWKJp1AEqgMaGLV+iocm
15 github.com/Azure/go-autorest/autorest/validation v0.1.0/go.mod h1:Ha3z/SqBeaalWQvokg3NZAlQTalVMtOIAs1aGK7G6u8=
16 github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
17 github.com/Azure/go-autorest/tracing v0.1.0/go.mod h1:ROEEAFwXycQw7Sn3DXNtEedEvdeRAgDr0izn4z5Ij88=
18 +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
19 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
20 github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg=
21 github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
@@ -134,6 +135,8 @@ github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV
135 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
136 github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
137 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
138 +github.com/jsternberg/zap-logfmt v1.2.0 h1:1v+PK4/B48cy8cfQbxL4FmmNZrjnIMr2BsnyEmXqv2o=
139 +github.com/jsternberg/zap-logfmt v1.2.0/go.mod h1:kz+1CUmCutPWABnNkOu9hOHKdT2q3TDYCcsFy9hpqb0=
140 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
141 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
142 github.com/keybase/go-ps v0.0.0-20190827175125-91aafc93ba19 h1:WjT3fLi9n8YWh/Ih8Q1LHAPsTqGddPcHqscN+PJ3i68=
@@ -201,13 +204,18 @@ github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXW
204 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
205 github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
206 github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
207 +github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w=
208 +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
209 github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
210 github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
211 +github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo=
212 +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
213 github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
214 github.com/oracle/oci-go-sdk v7.0.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35ukwStZIg5F66tcBccjip/j888=
215 github.com/ovh/go-ovh v0.0.0-20181109152953-ba5adb4cf014/go.mod h1:joRatxRJaZBsY3JAOEMcoOp05CnZzsx4scTxi95DHyQ=
216 github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
217 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
218 +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
219 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
220 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
221 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -268,8 +276,15 @@ go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
276 go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
277 go.starlark.net v0.0.0-20190919145610-979af19b165c h1:WR7X1xgXJlXhQBdorVc9Db3RhwG+J/kp6bLuMyJjfVw=
278 go.starlark.net v0.0.0-20190919145610-979af19b165c/go.mod h1:c1/X6cHgvdXj6pUlmWKMkuqRnW4K8x2vwt6JAaaircg=
279 +go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
280 go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
281 +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
282 +go.uber.org/multierr v1.2.0 h1:6I+W7f5VwC5SV9dNrZ3qXrDB9mD0dyGOi/ZJmYw03T4=
283 +go.uber.org/multierr v1.2.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
284 go.uber.org/ratelimit v0.0.0-20180316092928-c15da0234277/go.mod h1:2X8KaoNd1J0lZV+PxJk/5+DGbO/tpwLR1m++a7FnB/Y=
285 +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
286 +go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM=
287 +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
288 golang.org/x/crypto v0.0.0-20180621125126-a49355c7e3f8/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
289 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
290 golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -369,6 +384,9 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy
384 gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE=
385 gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
386 gopkg.in/ini.v1 v1.44.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
387 +gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
388 +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
389 +gopkg.in/ns1/ns1-go.v2 v2.0.0-20190730140822-b51389932cbc h1:GAcf+t0o8gdJAdSFYdE9wChu4bIyguMVqz0RHiFL5VY=
390 gopkg.in/ns1/ns1-go.v2 v2.0.0-20190730140822-b51389932cbc/go.mod h1:VV+3haRsgDiVLxyifmMBrBIuCWFBPYKbRssXB9z67Hw=
391 gopkg.in/resty.v1 v1.9.1/go.mod h1:vo52Hzryw9PnPHcJfPsBiFW62XhNx5OczbV9y+IMpgc=
392 gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
logging.go new
+610
@@ -0,0 +1,610 @@
1 +// Copyright 2015 Matthew Holt and The Caddy Authors
2 +//
3 +// Licensed under the Apache License, Version 2.0 (the "License");
4 +// you may not use this file except in compliance with the License.
5 +// You may obtain a copy of the License at
6 +//
7 +// http://www.apache.org/licenses/LICENSE-2.0
8 +//
9 +// Unless required by applicable law or agreed to in writing, software
10 +// distributed under the License is distributed on an "AS IS" BASIS,
11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 +// See the License for the specific language governing permissions and
13 +// limitations under the License.
14 +
15 +package caddy
16 +
17 +import (
18 + "encoding/json"
19 + "fmt"
20 + "io"
21 + "io/ioutil"
22 + "log"
23 + "os"
24 + "strings"
25 + "sync"
26 + "time"
27 +
28 + "go.uber.org/zap"
29 + "go.uber.org/zap/zapcore"
30 +)
31 +
32 +func init() {
33 + RegisterModule(StdoutWriter{})
34 + RegisterModule(StderrWriter{})
35 + RegisterModule(DiscardWriter{})
36 +}
37 +
38 +// Logging facilitates logging within Caddy.
39 +type Logging struct {
40 + Sink *StandardLibLog `json:"sink,omitempty"`
41 + Logs map[string]*CustomLog `json:"logs,omitempty"`
42 +
43 + // a list of all keys for open writers; all writers
44 + // that are opened to provision this logging config
45 + // must have their keys added to this list so they
46 + // can be closed when cleaning up
47 + writerKeys []string
48 +}
49 +
50 +// openLogs sets up the config and opens all the configured writers.
51 +// It closes its logs when ctx is cancelled, so it should clean up
52 +// after itself.
53 +func (logging *Logging) openLogs(ctx Context) error {
54 + // make sure to deallocate resources when context is done
55 + ctx.OnCancel(func() {
56 + err := logging.closeLogs()
57 + if err != nil {
58 + Log().Error("closing logs", zap.Error(err))
59 + }
60 + })
61 +
62 + // set up the "sink" log first (std lib's default global logger)
63 + if logging.Sink != nil {
64 + err := logging.Sink.provision(ctx, logging)
65 + if err != nil {
66 + return fmt.Errorf("setting up sink log: %v", err)
67 + }
68 + }
69 +
70 + // as a special case, set up the default structured Caddy log next
71 + if err := logging.setupNewDefault(ctx); err != nil {
72 + return err
73 + }
74 +
75 + // then set up any other custom logs
76 + for name, l := range logging.Logs {
77 + // the default log is already set up
78 + if name == "default" {
79 + continue
80 + }
81 +
82 + err := l.provision(ctx, logging)
83 + if err != nil {
84 + return fmt.Errorf("setting up custom log '%s': %v", name, err)
85 + }
86 +
87 + // Any other logs that use the discard writer can be deleted
88 + // entirely. This avoids encoding and processing of each
89 + // log entry that would just be thrown away anyway. Notably,
90 + // we do not reach this point for the default log, which MUST
91 + // exist, otherwise core log emissions would panic because
92 + // they use the Log() function directly which expects a non-nil
93 + // logger. Even if we keep logs with a discard writer, they
94 + // have a nop core, and keeping them at all seems unnecessary.
95 + if _, ok := l.writerOpener.(*DiscardWriter); ok {
96 + delete(logging.Logs, name)
97 + continue
98 + }
99 + }
100 +
101 + return nil
102 +}
103 +
104 +func (logging *Logging) setupNewDefault(ctx Context) error {
105 + if logging.Logs == nil {
106 + logging.Logs = make(map[string]*CustomLog)
107 + }
108 +
109 + // extract the user-defined default log, if any
110 + newDefault := new(defaultCustomLog)
111 + if userDefault, ok := logging.Logs["default"]; ok {
112 + newDefault.CustomLog = userDefault
113 + } else {
114 + // if none, make one with our own default settings
115 + var err error
116 + newDefault, err = newDefaultProductionLog()
117 + if err != nil {
118 + return fmt.Errorf("setting up default Caddy log: %v", err)
119 + }
120 + logging.Logs["default"] = newDefault.CustomLog
121 + }
122 +
123 + // set up this new log
124 + err := newDefault.CustomLog.provision(ctx, logging)
125 + if err != nil {
126 + return fmt.Errorf("setting up default log: %v", err)
127 + }
128 + newDefault.logger = zap.New(newDefault.CustomLog.core)
129 +
130 + // redirect the default caddy logs
131 + defaultLoggerMu.Lock()
132 + oldDefault := defaultLogger
133 + defaultLogger = newDefault
134 + defaultLoggerMu.Unlock()
135 +
136 + // if the new writer is different, indicate it in the logs for convenience
137 + var newDefaultLogWriterKey, currentDefaultLogWriterKey string
138 + var newDefaultLogWriterStr, currentDefaultLogWriterStr string
139 + if newDefault.writerOpener != nil {
140 + newDefaultLogWriterKey = newDefault.writerOpener.WriterKey()
141 + newDefaultLogWriterStr = newDefault.writerOpener.String()
142 + }
143 + if oldDefault.writerOpener != nil {
144 + currentDefaultLogWriterKey = oldDefault.writerOpener.WriterKey()
145 + currentDefaultLogWriterStr = oldDefault.writerOpener.String()
146 + }
147 + if newDefaultLogWriterKey != currentDefaultLogWriterKey {
148 + oldDefault.logger.Info("redirected default logger",
149 + zap.String("from", currentDefaultLogWriterStr),
150 + zap.String("to", newDefaultLogWriterStr),
151 + )
152 + }
153 +
154 + return nil
155 +}
156 +
157 +// closeLogs cleans up resources allocated during openLogs.
158 +// A successful call to openLogs calls this automatically
159 +// when the context is cancelled.
160 +func (logging *Logging) closeLogs() error {
161 + for _, key := range logging.writerKeys {
162 + _, err := writers.Delete(key)
163 + if err != nil {
164 + log.Printf("[ERROR] Closing log writer %v: %v", key, err)
165 + }
166 + }
167 + return nil
168 +}
169 +
170 +// Logger returns a logger that is ready for the module to use.
171 +func (logging *Logging) Logger(mod Module) *zap.Logger {
172 + modName := mod.CaddyModule().Name
173 + var cores []zapcore.Core
174 +
175 + if logging != nil {
176 + for _, l := range logging.Logs {
177 + if l.matchesModule(modName) {
178 + if len(l.Include) == 0 && len(l.Exclude) == 0 {
179 + cores = append(cores, l.core)
180 + continue
181 + }
182 + cores = append(cores, &filteringCore{Core: l.core, cl: l})
183 + }
184 + }
185 + }
186 +
187 + multiCore := zapcore.NewTee(cores...)
188 +
189 + return zap.New(multiCore).Named(modName)
190 +}
191 +
192 +// openWriter opens a writer using opener, and returns true if
193 +// the writer is new, or false if the writer already exists.
194 +func (logging *Logging) openWriter(opener WriterOpener) (io.WriteCloser, bool, error) {
195 + key := opener.WriterKey()
196 + writer, loaded, err := writers.LoadOrNew(key, func() (Destructor, error) {
197 + w, err := opener.OpenWriter()
198 + return writerDestructor{w}, err
199 + })
200 + if err == nil {
201 + logging.writerKeys = append(logging.writerKeys, key)
202 + }
203 + return writer.(io.WriteCloser), !loaded, err
204 +}
205 +
206 +// WriterOpener is a module that can open a log writer.
207 +// It can return a human-readable string representation
208 +// of itself so that operators can understand where
209 +// the logs are going.
210 +type WriterOpener interface {
211 + fmt.Stringer
212 +
213 + // WriterKey is a string that uniquely identifies this
214 + // writer configuration. It is not shown to humans.
215 + WriterKey() string
216 +
217 + // OpenWriter opens a log for writing. The writer
218 + // should be safe for concurrent use but need not
219 + // be synchronous.
220 + OpenWriter() (io.WriteCloser, error)
221 +}
222 +
223 +type writerDestructor struct {
224 + io.WriteCloser
225 +}
226 +
227 +func (wdest writerDestructor) Destruct() error {
228 + return wdest.Close()
229 +}
230 +
231 +// StandardLibLog configures the default Go standard library
232 +// global logger in the log package. This is necessary because
233 +// module dependencies which are not built specifically for
234 +// Caddy will use the standard logger.
235 +type StandardLibLog struct {
236 + WriterRaw json.RawMessage `json:"writer,omitempty"`
237 +
238 + writer io.WriteCloser
239 +}
240 +
241 +func (sll *StandardLibLog) provision(ctx Context, logging *Logging) error {
242 + if sll.WriterRaw != nil {
243 + val, err := ctx.LoadModuleInline("output", "caddy.logging.writers", sll.WriterRaw)
244 + if err != nil {
245 + return fmt.Errorf("loading sink log writer module: %v", err)
246 + }
247 + wo := val.(WriterOpener)
248 + sll.WriterRaw = nil // allow GC to deallocate
249 +
250 + var isNew bool
251 + sll.writer, isNew, err = logging.openWriter(wo)
252 + if err != nil {
253 + return fmt.Errorf("opening sink log writer %#v: %v", val, err)
254 + }
255 +
256 + if isNew {
257 + log.Printf("[INFO] Redirecting sink to: %s", wo)
258 + log.SetOutput(sll.writer)
259 + log.Printf("[INFO] Redirected sink to here (%s)", wo)
260 + }
261 + }
262 +
263 + return nil
264 +}
265 +
266 +// CustomLog represents a custom logger configuration.
267 +type CustomLog struct {
268 + WriterRaw json.RawMessage `json:"writer,omitempty"`
269 + EncoderRaw json.RawMessage `json:"encoder,omitempty"`
270 + Level string `json:"level,omitempty"`
271 + Sampling *LogSampling `json:"sampling,omitempty"`
272 + Include []string `json:"include,omitempty"`
273 + Exclude []string `json:"exclude,omitempty"`
274 +
275 + writerOpener WriterOpener
276 + writer io.WriteCloser
277 + encoder zapcore.Encoder
278 + levelEnabler zapcore.LevelEnabler
279 + core zapcore.Core
280 +}
281 +
282 +func (cl *CustomLog) provision(ctx Context, logging *Logging) error {
283 + // set up the log level
284 + switch cl.Level {
285 + case "debug":
286 + cl.levelEnabler = zapcore.DebugLevel
287 + case "", "info":
288 + cl.levelEnabler = zapcore.InfoLevel
289 + case "warn":
290 + cl.levelEnabler = zapcore.WarnLevel
291 + case "error":
292 + cl.levelEnabler = zapcore.ErrorLevel
293 + case "panic":
294 + cl.levelEnabler = zapcore.PanicLevel
295 + case "fatal":
296 + cl.levelEnabler = zapcore.FatalLevel
297 + default:
298 + return fmt.Errorf("unrecognized log level: %s", cl.Level)
299 + }
300 +
301 + // If both Include and Exclude lists are populated, then each item must
302 + // be a superspace or subspace of an item in the other list, because
303 + // populating both lists means that any given item is either a rule
304 + // or an exception to another rule. But if the item is not a super-
305 + // or sub-space of any item in the other list, it is neither a rule
306 + // nor an exception, and is a contradiction. Ensure, too, that the
307 + // sets do not intersect, which is also a contradiction.
308 + if len(cl.Include) > 0 && len(cl.Exclude) > 0 {
309 + // prevent intersections
310 + for _, allow := range cl.Include {
311 + for _, deny := range cl.Exclude {
312 + if allow == deny {
313 + return fmt.Errorf("include and exclude must not intersect, but found %s in both lists", allow)
314 + }
315 + }
316 + }
317 +
318 + // ensure namespaces are nested
319 + outer:
320 + for _, allow := range cl.Include {
321 + for _, deny := range cl.Exclude {
322 + if strings.HasPrefix(allow+".", deny+".") ||
323 + strings.HasPrefix(deny+".", allow+".") {
324 + continue outer
325 + }
326 + }
327 + return fmt.Errorf("when both include and exclude are populated, each element must be a superspace or subspace of one in the other list; check '%s' in include", allow)
328 + }
329 + }
330 +
331 + if cl.EncoderRaw != nil {
332 + val, err := ctx.LoadModuleInline("format", "caddy.logging.encoders", cl.EncoderRaw)
333 + if err != nil {
334 + return fmt.Errorf("loading log encoder module: %v", err)
335 + }
336 + cl.EncoderRaw = nil // allow GC to deallocate
337 + cl.encoder = val.(zapcore.Encoder)
338 + }
339 + if cl.encoder == nil {
340 + cl.encoder = zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig())
341 + }
342 +
343 + if cl.WriterRaw != nil {
344 + val, err := ctx.LoadModuleInline("output", "caddy.logging.writers", cl.WriterRaw)
345 + if err != nil {
346 + return fmt.Errorf("loading log writer module: %v", err)
347 + }
348 + cl.WriterRaw = nil // allow GC to deallocate
349 + cl.writerOpener = val.(WriterOpener)
350 + }
351 + if cl.writerOpener == nil {
352 + cl.writerOpener = StderrWriter{}
353 + }
354 + var err error
355 + cl.writer, _, err = logging.openWriter(cl.writerOpener)
356 + if err != nil {
357 + return fmt.Errorf("opening log writer using %#v: %v", cl.writerOpener, err)
358 + }
359 +
360 + cl.buildCore()
361 +
362 + return nil
363 +}
364 +
365 +func (cl *CustomLog) buildCore() {
366 + // logs which only discard their output don't need
367 + // to perform encoding or any other processing steps
368 + // at all, so just shorcut to a nop core instead
369 + if _, ok := cl.writerOpener.(*DiscardWriter); ok {
370 + cl.core = zapcore.NewNopCore()
371 + return
372 + }
373 + c := zapcore.NewCore(
374 + cl.encoder,
375 + zapcore.AddSync(cl.writer),
376 + cl.levelEnabler,
377 + )
378 + if cl.Sampling != nil {
379 + if cl.Sampling.Interval == 0 {
380 + cl.Sampling.Interval = 1 * time.Second
381 + }
382 + if cl.Sampling.First == 0 {
383 + cl.Sampling.First = 100
384 + }
385 + if cl.Sampling.Thereafter == 0 {
386 + cl.Sampling.Thereafter = 100
387 + }
388 + c = zapcore.NewSampler(c, cl.Sampling.Interval,
389 + cl.Sampling.First, cl.Sampling.Thereafter)
390 + }
391 + cl.core = c
392 +}
393 +
394 +func (cl *CustomLog) matchesModule(moduleName string) bool {
395 + return cl.loggerAllowed(moduleName, true)
396 +}
397 +
398 +// loggerAllowed returns true if name is allowed to emit
399 +// to cl. isModule should be true if name is the name of
400 +// a module and you want to see if ANY of that module's
401 +// logs would be permitted.
402 +func (cl *CustomLog) loggerAllowed(name string, isModule bool) bool {
403 + // accept all loggers by default
404 + if len(cl.Include) == 0 && len(cl.Exclude) == 0 {
405 + return true
406 + }
407 +
408 + // append a dot so that partial names don't match
409 + // (i.e. we don't want "foo.b" to match "foo.bar"); we
410 + // will also have to append a dot when we do HasPrefix
411 + // below to compensate for when when namespaces are equal
412 + if name != "" && name != "*" && name != "." {
413 + name += "."
414 + }
415 +
416 + var longestAccept, longestReject int
417 +
418 + if len(cl.Include) > 0 {
419 + for _, namespace := range cl.Include {
420 + var hasPrefix bool
421 + if isModule {
422 + hasPrefix = strings.HasPrefix(namespace+".", name)
423 + } else {
424 + hasPrefix = strings.HasPrefix(name, namespace+".")
425 + }
426 + if hasPrefix && len(namespace) > longestAccept {
427 + longestAccept = len(namespace)
428 + }
429 + }
430 + // the include list was populated, meaning that
431 + // a match in this list is absolutely required
432 + // if we are to accept the entry
433 + if longestAccept == 0 {
434 + return false
435 + }
436 + }
437 +
438 + if len(cl.Exclude) > 0 {
439 + for _, namespace := range cl.Exclude {
440 + // * == all logs emitted by modules
441 + // . == all logs emitted by core
442 + if (namespace == "*" && name != ".") ||
443 + (namespace == "." && name == ".") {
444 + return false
445 + }
446 + if strings.HasPrefix(name, namespace+".") &&
447 + len(namespace) > longestReject {
448 + longestReject = len(namespace)
449 + }
450 + }
451 + // the reject list is populated, so we have to
452 + // reject this entry if its match is better
453 + // than the best from the accept list
454 + if longestReject > longestAccept {
455 + return false
456 + }
457 + }
458 +
459 + return (longestAccept > longestReject) ||
460 + (len(cl.Include) == 0 && longestReject == 0)
461 +}
462 +
463 +// filteringCore filters log entries based on logger name,
464 +// according to the rules of a CustomLog.
465 +type filteringCore struct {
466 + zapcore.Core
467 + cl *CustomLog
468 +}
469 +
470 +// With properly wraps With.
471 +func (fc *filteringCore) With(fields []zapcore.Field) zapcore.Core {
472 + return &filteringCore{
473 + Core: fc.Core.With(fields),
474 + cl: fc.cl,
475 + }
476 +}
477 +
478 +// Check only allows the log entry if its logger name
479 +// is allowed from the include/exclude rules of fc.cl.
480 +func (fc *filteringCore) Check(e zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry {
481 + if fc.cl.loggerAllowed(e.LoggerName, false) {
482 + return fc.Core.Check(e, ce)
483 + }
484 + return ce
485 +}
486 +
487 +// LogSampling configures log entry sampling.
488 +type LogSampling struct {
489 + Interval time.Duration `json:"interval,omitempty"`
490 + First int `json:"first,omitempty"`
491 + Thereafter int `json:"thereafter,omitempty"`
492 +}
493 +
494 +type (
495 + // StdoutWriter can write logs to stdout.
496 + StdoutWriter struct{}
497 +
498 + // StderrWriter can write logs to stdout.
499 + StderrWriter struct{}
500 +
501 + // DiscardWriter discards all writes.
502 + DiscardWriter struct{}
503 +)
504 +
505 +// CaddyModule returns the Caddy module information.
506 +func (StdoutWriter) CaddyModule() ModuleInfo {
507 + return ModuleInfo{
508 + Name: "caddy.logging.writers.stdout",
509 + New: func() Module { return new(StdoutWriter) },
510 + }
511 +}
512 +
513 +// CaddyModule returns the Caddy module information.
514 +func (StderrWriter) CaddyModule() ModuleInfo {
515 + return ModuleInfo{
516 + Name: "caddy.logging.writers.stderr",
517 + New: func() Module { return new(StderrWriter) },
518 + }
519 +}
520 +
521 +// CaddyModule returns the Caddy module information.
522 +func (DiscardWriter) CaddyModule() ModuleInfo {
523 + return ModuleInfo{
524 + Name: "caddy.logging.writers.discard",
525 + New: func() Module { return new(DiscardWriter) },
526 + }
527 +}
528 +
529 +func (StdoutWriter) String() string { return "stdout" }
530 +func (StderrWriter) String() string { return "stderr" }
531 +func (DiscardWriter) String() string { return "discard" }
532 +
533 +// WriterKey returns a unique key representing stdout.
534 +func (StdoutWriter) WriterKey() string { return "std:out" }
535 +
536 +// WriterKey returns a unique key representing stderr.
537 +func (StderrWriter) WriterKey() string { return "std:err" }
538 +
539 +// WriterKey returns a unique key representing discard.
540 +func (DiscardWriter) WriterKey() string { return "discard" }
541 +
542 +// OpenWriter returns os.Stdout that can't be closed.
543 +func (StdoutWriter) OpenWriter() (io.WriteCloser, error) {
544 + return notClosable{os.Stdout}, nil
545 +}
546 +
547 +// OpenWriter returns os.Stderr that can't be closed.
548 +func (StderrWriter) OpenWriter() (io.WriteCloser, error) {
549 + return notClosable{os.Stderr}, nil
550 +}
551 +
552 +// OpenWriter returns ioutil.Discard that can't be closed.
553 +func (DiscardWriter) OpenWriter() (io.WriteCloser, error) {
554 + return notClosable{ioutil.Discard}, nil
555 +}
556 +
557 +// notClosable is an io.WriteCloser that can't be closed.
558 +type notClosable struct{ io.Writer }
559 +
560 +func (fc notClosable) Close() error { return nil }
561 +
562 +type defaultCustomLog struct {
563 + *CustomLog
564 + logger *zap.Logger
565 +}
566 +
567 +// newDefaultProductionLog configures a custom log that is
568 +// intended for use by default if no other log is specified
569 +// in a config. It writes to stderr, uses the console encoder,
570 +// and enables INFO-level logs and higher.
571 +func newDefaultProductionLog() (*defaultCustomLog, error) {
572 + cl := new(CustomLog)
573 + cl.writerOpener = StderrWriter{}
574 + var err error
575 + cl.writer, err = cl.writerOpener.OpenWriter()
576 + if err != nil {
577 + return nil, err
578 + }
579 + encCfg := zap.NewProductionEncoderConfig()
580 + cl.encoder = zapcore.NewConsoleEncoder(encCfg)
581 + cl.levelEnabler = zapcore.InfoLevel
582 +
583 + cl.buildCore()
584 +
585 + return &defaultCustomLog{
586 + CustomLog: cl,
587 + logger: zap.New(cl.core),
588 + }, nil
589 +}
590 +
591 +// Log returns the current default logger.
592 +func Log() *zap.Logger {
593 + defaultLoggerMu.RLock()
594 + defer defaultLoggerMu.RUnlock()
595 + return defaultLogger.logger
596 +}
597 +
598 +var (
599 + defaultLogger, _ = newDefaultProductionLog()
600 + defaultLoggerMu sync.RWMutex
601 +)
602 +
603 +var writers = NewUsagePool()
604 +
605 +// Interface guards
606 +var (
607 + _ io.WriteCloser = (*notClosable)(nil)
608 + _ WriterOpener = (*StdoutWriter)(nil)
609 + _ WriterOpener = (*StderrWriter)(nil)
610 +)
modules/caddyhttp/caddyhttp.go
+50 -22
@@ -21,7 +21,6 @@ import (
21 "encoding/json"
22 "fmt"
23 "io"
24 - "log"
24 weakrand "math/rand"
25 "net"
26 "net/http"
@@ -33,6 +32,7 @@ import (
32 "github.com/caddyserver/caddy/v2/modules/caddytls"
33 "github.com/lucas-clemente/quic-go/http3"
34 "github.com/mholt/certmagic"
35 + "go.uber.org/zap"
36 )
37
38 func init() {
@@ -40,7 +40,7 @@ func init() {
40
41 err := caddy.RegisterModule(App{})
42 if err != nil {
43 - log.Fatal(err)
43 + caddy.Log().Fatal(err.Error())
44 }
45 }
46
@@ -55,7 +55,8 @@ type App struct {
55 h3servers []*http3.Server
56 h3listeners []net.PacketConn
57
58 - ctx caddy.Context
58 + ctx caddy.Context
59 + logger *zap.Logger
60 }
61
62 // CaddyModule returns the Caddy module information.
@@ -69,10 +70,15 @@ func (App) CaddyModule() caddy.ModuleInfo {
70 // Provision sets up the app.
71 func (app *App) Provision(ctx caddy.Context) error {
72 app.ctx = ctx
73 + app.logger = ctx.Logger(app)
74
75 repl := caddy.NewReplacer()
76
75 - for _, srv := range app.Servers {
77 + for srvName, srv := range app.Servers {
78 + srv.logger = app.logger.Named("log")
79 + srv.accessLogger = app.logger.Named("log.access")
80 + srv.errorLogger = app.logger.Named("log.error")
81 +
82 if srv.AutoHTTPS == nil {
83 // avoid nil pointer dereferences
84 srv.AutoHTTPS = new(AutoHTTPSConfig)
@@ -93,20 +99,25 @@ func (app *App) Provision(ctx caddy.Context) error {
99 }
100
101 for i := range srv.Listen {
96 - srv.Listen[i] = repl.ReplaceAll(srv.Listen[i], "")
102 + lnOut, err := repl.ReplaceOrErr(srv.Listen[i], true, true)
103 + if err != nil {
104 + return fmt.Errorf("server %s, listener %d: %v",
105 + srvName, i, err)
106 + }
107 + srv.Listen[i] = lnOut
108 }
109
110 if srv.Routes != nil {
111 err := srv.Routes.Provision(ctx)
112 if err != nil {
102 - return fmt.Errorf("setting up server routes: %v", err)
113 + return fmt.Errorf("server %s: setting up server routes: %v", srvName, err)
114 }
115 }
116
117 if srv.Errors != nil {
118 err := srv.Errors.Routes.Provision(ctx)
119 if err != nil {
109 - return fmt.Errorf("setting up server error handling routes: %v", err)
120 + return fmt.Errorf("server %s: setting up server error handling routes: %v", srvName, err)
121 }
122 }
123
@@ -194,7 +205,9 @@ func (app *App) Start() error {
205 /////////
206 // TODO: HTTP/3 support is experimental for now
207 if srv.ExperimentalHTTP3 {
197 - log.Printf("[INFO] Enabling experimental HTTP/3 listener on %s", addr)
208 + app.logger.Info("enabling experimental HTTP/3 listener",
209 + zap.String("addr", addr),
210 + )
211 h3ln, err := caddy.ListenPacket("udp", addr)
212 if err != nil {
213 return fmt.Errorf("getting HTTP/3 UDP listener: %v", err)
@@ -284,19 +297,25 @@ func (app *App) automaticHTTPS() error {
297
298 // skip if all listeners use the HTTP port
299 if !srv.listenersUseAnyPortOtherThan(app.httpPort()) {
287 - log.Printf("[INFO] Server %s is only listening on the HTTP port %d, so no automatic HTTPS will be applied to this server",
288 - srvName, app.httpPort())
300 + app.logger.Info("server is only listening on the HTTP port, so no automatic HTTPS will be applied to this server",
301 + zap.String("server_name", srvName),
302 + zap.Int("http_port", app.httpPort()),
303 + )
304 continue
305 }
306
307 // find all qualifying domain names, de-duplicated
308 domainSet := make(map[string]struct{})
294 - for _, route := range srv.Routes {
295 - for _, matcherSet := range route.MatcherSets {
296 - for _, m := range matcherSet {
309 + for routeIdx, route := range srv.Routes {
310 + for matcherSetIdx, matcherSet := range route.MatcherSets {
311 + for matcherIdx, m := range matcherSet {
312 if hm, ok := m.(*MatchHost); ok {
298 - for _, d := range *hm {
299 - d = repl.ReplaceAll(d, "")
313 + for hostMatcherIdx, d := range *hm {
314 + d, err = repl.ReplaceOrErr(d, true, false)
315 + if err != nil {
316 + return fmt.Errorf("%s: route %d, matcher set %d, matcher %d, host matcher %d: %v",
317 + srvName, routeIdx, matcherSetIdx, matcherIdx, hostMatcherIdx, err)
318 + }
319 if certmagic.HostQualifies(d) &&
320 !srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.Skip) {
321 domainSet[d] = struct{}{}
@@ -318,7 +337,10 @@ func (app *App) automaticHTTPS() error {
337 // supposed to ignore loaded certificates
338 if !srv.AutoHTTPS.IgnoreLoadedCerts &&
339 len(tlsApp.AllMatchingCertificates(d)) > 0 {
321 - log.Printf("[INFO][%s] Skipping automatic certificate management because one or more matching certificates are already loaded", d)
340 + app.logger.Info("skipping automatic certificate management because one or more matching certificates are already loaded",
341 + zap.String("domain", d),
342 + zap.String("server_name", srvName),
343 + )
344 continue
345 }
346 domainsForCerts = append(domainsForCerts, d)
@@ -351,7 +373,9 @@ func (app *App) automaticHTTPS() error {
373 })
374
375 // manage their certificates
354 - log.Printf("[INFO] Enabling automatic TLS certificate management for %v", domainsForCerts)
376 + app.logger.Info("enabling automatic TLS certificate management",
377 + zap.Strings("domains", domainsForCerts),
378 + )
379 err := tlsApp.Manage(domainsForCerts)
380 if err != nil {
381 return fmt.Errorf("%s: managing certificate for %s: %s", srvName, domains, err)
@@ -368,7 +392,9 @@ func (app *App) automaticHTTPS() error {
392 continue
393 }
394
371 - log.Printf("[INFO] Enabling automatic HTTP->HTTPS redirects for %v", domains)
395 + app.logger.Info("enabling automatic HTTP->HTTPS redirects",
396 + zap.Strings("domains", domains),
397 + )
398
399 // create HTTP->HTTPS redirects
400 for _, addr := range srv.Listen {
@@ -434,8 +460,10 @@ func (app *App) automaticHTTPS() error {
460 // that the redirect runs from; simply append our
461 // redirect route to the existing routes, with a
462 // caveat that their config might override ours
437 - log.Printf("[WARNING] Server %s is listening on %s, so automatic HTTP->HTTPS redirects might be overridden by your own configuration",
438 - srvName, addr)
463 + app.logger.Warn("server is listening on same interface as redirects, so automatic HTTP->HTTPS redirects might be overridden by your own configuration",
464 + zap.String("server_name", srvName),
465 + zap.String("interface", addr),
466 + )
467 srv.Routes = append(srv.Routes, redirRoute)
468 continue redirRoutesLoop
469 }
@@ -524,8 +552,8 @@ var emptyHandler HandlerFunc = func(http.ResponseWriter, *http.Request) error {
552
553 // WeakString is a type that unmarshals any JSON value
554 // as a string literal, with the following exceptions:
527 -// 1) actual string values are decoded as strings, and
528 -// 2) null is decoded as empty string
555 +// 1) actual string values are decoded as strings; and
556 +// 2) null is decoded as empty string;
557 // and provides methods for getting the value as various
558 // primitive types. However, using this type removes any
559 // type safety as far as deserializing JSON is concerned.
modules/caddyhttp/fileserver/staticfiles.go
+19
@@ -18,6 +18,7 @@ import (
18 "bytes"
19 "fmt"
20 "html/template"
21 + "io"
22 weakrand "math/rand"
23 "mime"
24 "net/http"
@@ -191,6 +192,24 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, _ cadd
192 }
193 }
194
195 + // if this handler exists in an error context (i.e. is
196 + // part of a handler chain that is supposed to handle
197 + // a previous error), we have to serve the content
198 + // manually in order to write the correct status code
199 + if reqErr, ok := r.Context().Value(caddyhttp.ErrorCtxKey).(error); ok {
200 + statusCode := http.StatusInternalServerError
201 + if handlerErr, ok := reqErr.(caddyhttp.HandlerError); ok {
202 + if handlerErr.StatusCode > 0 {
203 + statusCode = handlerErr.StatusCode
204 + }
205 + }
206 + w.WriteHeader(statusCode)
207 + if r.Method != "HEAD" {
208 + io.Copy(w, file)
209 + }
210 + return nil
211 + }
212 +
213 // let the standard library do what it does best; note, however,
214 // that errors generated by ServeContent are written immediately
215 // to the response, so we cannot handle them (but errors there
modules/caddyhttp/marshalers.go new
+89
@@ -0,0 +1,89 @@
1 +// Copyright 2015 Matthew Holt and The Caddy Authors
2 +//
3 +// Licensed under the Apache License, Version 2.0 (the "License");
4 +// you may not use this file except in compliance with the License.
5 +// You may obtain a copy of the License at
6 +//
7 +// http://www.apache.org/licenses/LICENSE-2.0
8 +//
9 +// Unless required by applicable law or agreed to in writing, software
10 +// distributed under the License is distributed on an "AS IS" BASIS,
11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 +// See the License for the specific language governing permissions and
13 +// limitations under the License.
14 +
15 +package caddyhttp
16 +
17 +import (
18 + "crypto/tls"
19 + "net/http"
20 +
21 + "go.uber.org/zap/zapcore"
22 +)
23 +
24 +// LoggableHTTPRequest makes an HTTP request loggable with zap.Object().
25 +type LoggableHTTPRequest struct{ *http.Request }
26 +
27 +// MarshalLogObject satisfies the zapcore.ObjectMarshaler interface.
28 +func (r LoggableHTTPRequest) MarshalLogObject(enc zapcore.ObjectEncoder) error {
29 + enc.AddString("method", r.Method)
30 + enc.AddString("uri", r.RequestURI)
31 + enc.AddString("proto", r.Proto)
32 + enc.AddString("remote_addr", r.RemoteAddr)
33 + enc.AddString("host", r.Host)
34 + enc.AddObject("headers", LoggableHTTPHeader(r.Header))
35 + if r.TLS != nil {
36 + enc.AddObject("tls", LoggableTLSConnState(*r.TLS))
37 + }
38 + return nil
39 +}
40 +
41 +// LoggableHTTPHeader makes an HTTP header loggable with zap.Object().
42 +type LoggableHTTPHeader http.Header
43 +
44 +// MarshalLogObject satisfies the zapcore.ObjectMarshaler interface.
45 +func (h LoggableHTTPHeader) MarshalLogObject(enc zapcore.ObjectEncoder) error {
46 + if h == nil {
47 + return nil
48 + }
49 + for key, val := range h {
50 + enc.AddArray(key, LoggableStringArray(val))
51 + }
52 + return nil
53 +}
54 +
55 +// LoggableStringArray makes a slice of strings marshalable for logging.
56 +type LoggableStringArray []string
57 +
58 +// MarshalLogArray satisfies the zapcore.ArrayMarshaler interface.
59 +func (sa LoggableStringArray) MarshalLogArray(enc zapcore.ArrayEncoder) error {
60 + if sa == nil {
61 + return nil
62 + }
63 + for _, s := range sa {
64 + enc.AppendString(s)
65 + }
66 + return nil
67 +}
68 +
69 +// LoggableTLSConnState makes a TLS connection state loggable with zap.Object().
70 +type LoggableTLSConnState tls.ConnectionState
71 +
72 +// MarshalLogObject satisfies the zapcore.ObjectMarshaler interface.
73 +func (t LoggableTLSConnState) MarshalLogObject(enc zapcore.ObjectEncoder) error {
74 + enc.AddBool("resumed", t.DidResume)
75 + enc.AddUint16("version", t.Version)
76 + enc.AddUint16("resumed", t.CipherSuite)
77 + enc.AddString("proto", t.NegotiatedProtocol)
78 + enc.AddBool("proto_mutual", t.NegotiatedProtocolIsMutual)
79 + enc.AddString("server_name", t.ServerName)
80 + return nil
81 +}
82 +
83 +// Interface guards
84 +var (
85 + _ zapcore.ObjectMarshaler = (*LoggableHTTPRequest)(nil)
86 + _ zapcore.ObjectMarshaler = (*LoggableHTTPHeader)(nil)
87 + _ zapcore.ArrayMarshaler = (*LoggableStringArray)(nil)
88 + _ zapcore.ObjectMarshaler = (*LoggableTLSConnState)(nil)
89 +)
modules/caddyhttp/replacer.go
+17 -15
@@ -19,7 +19,6 @@ import (
19 "net"
20 "net/http"
21 "net/textproto"
22 - "net/url"
22 "path"
23 "strconv"
24 "strings"
@@ -112,27 +111,30 @@ func addHTTPVarsToReplacer(repl caddy.Replacer, req *http.Request, w http.Respon
111 }
112 return qs, true
113
115 - // original URI, before any internal changes
114 + // original request, before any internal changes
115 + case "http.request.orig_method":
116 + or, _ := req.Context().Value(OriginalRequestCtxKey).(http.Request)
117 + return or.Method, true
118 case "http.request.orig_uri":
117 - u, _ := req.Context().Value(OriginalURLCtxKey).(url.URL)
118 - return u.RequestURI(), true
119 + or, _ := req.Context().Value(OriginalRequestCtxKey).(http.Request)
120 + return or.RequestURI, true
121 case "http.request.orig_uri.path":
120 - u, _ := req.Context().Value(OriginalURLCtxKey).(url.URL)
121 - return u.Path, true
122 + or, _ := req.Context().Value(OriginalRequestCtxKey).(http.Request)
123 + return or.URL.Path, true
124 case "http.request.orig_uri.path.file":
123 - u, _ := req.Context().Value(OriginalURLCtxKey).(url.URL)
124 - _, file := path.Split(u.Path)
125 + or, _ := req.Context().Value(OriginalRequestCtxKey).(http.Request)
126 + _, file := path.Split(or.URL.Path)
127 return file, true
128 case "http.request.orig_uri.path.dir":
127 - u, _ := req.Context().Value(OriginalURLCtxKey).(url.URL)
128 - dir, _ := path.Split(u.Path)
129 + or, _ := req.Context().Value(OriginalRequestCtxKey).(http.Request)
130 + dir, _ := path.Split(or.URL.Path)
131 return dir, true
132 case "http.request.orig_uri.query":
131 - u, _ := req.Context().Value(OriginalURLCtxKey).(url.URL)
132 - return u.RawQuery, true
133 + or, _ := req.Context().Value(OriginalRequestCtxKey).(http.Request)
134 + return or.URL.RawQuery, true
135 case "http.request.orig_uri.query_string":
134 - u, _ := req.Context().Value(OriginalURLCtxKey).(url.URL)
135 - qs := u.Query().Encode()
136 + or, _ := req.Context().Value(OriginalRequestCtxKey).(http.Request)
137 + qs := or.URL.Query().Encode()
138 if qs != "" {
139 qs = "?" + qs
140 }
@@ -183,7 +185,7 @@ func addHTTPVarsToReplacer(repl caddy.Replacer, req *http.Request, w http.Respon
185 // middleware variables
186 if strings.HasPrefix(key, varsReplPrefix) {
187 varName := key[len(varsReplPrefix):]
186 - tbl := req.Context().Value(VarCtxKey).(map[string]interface{})
188 + tbl := req.Context().Value(VarsCtxKey).(map[string]interface{})
189 raw, ok := tbl[varName]
190 if !ok {
191 // variables can be dynamic, so always return true
modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
+6 -3
@@ -19,7 +19,6 @@ import (
19 "crypto/tls"
20 "fmt"
21 "net/http"
22 - "net/url"
22 "path"
23 "path/filepath"
24 "strconv"
@@ -53,6 +52,9 @@ type Transport struct {
52 // with the value of SplitPath. The first piece will be assumed as the
53 // actual resource (CGI script) name, and the second piece will be set to
54 // PATH_INFO for the CGI script to use.
55 + // Future enhancements should be careful to avoid CVE-2019-11043,
56 + // which can be mitigated with use of a try_files-like behavior
57 + // that 404's if the fastcgi path info is not found.
58 SplitPath string `json:"split_path,omitempty"`
59
60 // Extra environment variables
@@ -191,12 +193,13 @@ func (t Transport) buildEnv(r *http.Request) (map[string]string, error) {
193 // original URI in as the value of REQUEST_URI (the user can overwrite this
194 // if desired). Most PHP apps seem to want the original URI. Besides, this is
195 // how nginx defaults: http://stackoverflow.com/a/12485156/1048862
194 - reqURL, ok := r.Context().Value(caddyhttp.OriginalURLCtxKey).(url.URL)
196 + origReq, ok := r.Context().Value(caddyhttp.OriginalRequestCtxKey).(http.Request)
197 if !ok {
198 // some requests, like active health checks, don't add this to
199 // the request context, so we can just use the current URL
198 - reqURL = *r.URL
200 + origReq = *r
201 }
202 + reqURL := origReq.URL
203
204 requestScheme := "http"
205 if r.TLS != nil {
modules/caddyhttp/reverseproxy/reverseproxy.go
+9
@@ -441,6 +441,15 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, di Dia
441 }
442 }
443
444 + // TODO: there should be an option to return an error if the response
445 + // matches some criteria; would solve https://github.com/caddyserver/caddy/issues/1447
446 + // by allowing the backend to determine whether this server should treat
447 + // a 400+ status code as an error -- but we might need to be careful that
448 + // we do not affect the health status of the backend... still looking into
449 + // that; if we need to avoid that, we should return a particular error type
450 + // that the caller of this function checks for and only applies health
451 + // status changes if the error is not this special type
452 +
453 rw.WriteHeader(res.StatusCode)
454
455 err = h.copyResponse(rw, res.Body, h.flushInterval(req, res))
modules/caddyhttp/rewrite/rewrite.go
+29 -11
@@ -23,6 +23,7 @@ import (
23
24 "github.com/caddyserver/caddy/v2"
25 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
26 + "go.uber.org/zap"
27 )
28
29 func init() {
@@ -39,6 +40,8 @@ type Rewrite struct {
40
41 HTTPRedirect caddyhttp.WeakString `json:"http_redirect,omitempty"`
42 Rehandle bool `json:"rehandle,omitempty"`
43 +
44 + logger *zap.Logger
45 }
46
47 // CaddyModule returns the Caddy module information.
@@ -49,6 +52,12 @@ func (Rewrite) CaddyModule() caddy.ModuleInfo {
52 }
53 }
54
55 +// Provision sets up rewr.
56 +func (rewr *Rewrite) Provision(ctx caddy.Context) error {
57 + rewr.logger = ctx.Logger(rewr)
58 + return nil
59 +}
60 +
61 // Validate ensures rewr's configuration is valid.
62 func (rewr Rewrite) Validate() error {
63 if rewr.HTTPRedirect != "" && rewr.Rehandle {
@@ -61,6 +70,10 @@ func (rewr Rewrite) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy
70 repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
71 var changed bool
72
73 + logger := rewr.logger.With(
74 + zap.Object("request", caddyhttp.LoggableHTTPRequest{Request: r}),
75 + )
76 +
77 // rewrite the method
78 if rewr.Method != "" {
79 method := r.Method
@@ -114,18 +127,23 @@ func (rewr Rewrite) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy
127 r.RequestURI = newURI
128 }
129
117 - if changed && rewr.Rehandle {
118 - return caddyhttp.ErrRehandle
119 - }
120 -
121 - if changed && rewr.HTTPRedirect != "" {
122 - statusCode, err := strconv.Atoi(repl.ReplaceAll(rewr.HTTPRedirect.String(), ""))
123 - if err != nil {
124 - return caddyhttp.Error(http.StatusInternalServerError, err)
130 + if changed {
131 + logger.Debug("rewrote request",
132 + zap.String("method", r.Method),
133 + zap.String("uri", r.RequestURI),
134 + )
135 + if rewr.Rehandle {
136 + return caddyhttp.ErrRehandle
137 + }
138 + if rewr.HTTPRedirect != "" {
139 + statusCode, err := strconv.Atoi(repl.ReplaceAll(rewr.HTTPRedirect.String(), ""))
140 + if err != nil {
141 + return caddyhttp.Error(http.StatusInternalServerError, err)
142 + }
143 + w.Header().Set("Location", r.RequestURI)
144 + w.WriteHeader(statusCode)
145 + return nil
146 }
126 - w.Header().Set("Location", r.RequestURI)
127 - w.WriteHeader(statusCode)
128 - return nil
147 }
148
149 return next.ServeHTTP(w, r)
modules/caddyhttp/server.go
+168 -39
@@ -17,16 +17,18 @@ package caddyhttp
17 import (
18 "context"
19 "fmt"
20 - "log"
20 "net"
21 "net/http"
22 "net/url"
23 "strconv"
24 "strings"
25 + "time"
26
27 "github.com/caddyserver/caddy/v2"
28 "github.com/caddyserver/caddy/v2/modules/caddytls"
29 "github.com/lucas-clemente/quic-go/http3"
30 + "go.uber.org/zap"
31 + "go.uber.org/zap/zapcore"
32 )
33
34 // Server is an HTTP server.
@@ -43,11 +45,15 @@ type Server struct {
45 AutoHTTPS *AutoHTTPSConfig `json:"automatic_https,omitempty"`
46 MaxRehandles *int `json:"max_rehandles,omitempty"`
47 StrictSNIHost *bool `json:"strict_sni_host,omitempty"`
48 + Logs *ServerLogConfig `json:"logs,omitempty"`
49
50 // This field is not subject to compatibility promises
51 ExperimentalHTTP3 bool `json:"experimental_http3,omitempty"`
52
50 - tlsApp *caddytls.TLS
53 + tlsApp *caddytls.TLS
54 + logger *zap.Logger
55 + accessLogger *zap.Logger
56 + errorLogger *zap.Logger
57
58 h3server *http3.Server
59 }
@@ -59,58 +65,106 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
65 if s.h3server != nil {
66 err := s.h3server.SetQuicHeaders(w.Header())
67 if err != nil {
62 - log.Printf("[ERROR] Setting HTTP/3 Alt-Svc header: %v", err)
68 + s.logger.Error("setting HTTP/3 Alt-Svc header", zap.Error(err))
69 }
70 }
71
66 - if s.tlsApp.HandleHTTPChallenge(w, r) {
67 - return
68 - }
69 -
72 // set up the context for the request
73 repl := caddy.NewReplacer()
74 ctx := context.WithValue(r.Context(), caddy.ReplacerCtxKey, repl)
75 ctx = context.WithValue(ctx, ServerCtxKey, s)
74 - ctx = context.WithValue(ctx, VarCtxKey, make(map[string]interface{}))
75 - ctx = context.WithValue(ctx, OriginalURLCtxKey, cloneURL(r.URL))
76 + ctx = context.WithValue(ctx, VarsCtxKey, make(map[string]interface{}))
77 + var url2 url.URL // avoid letting this escape to the heap
78 + ctx = context.WithValue(ctx, OriginalRequestCtxKey, originalRequest(r, &url2))
79 r = r.WithContext(ctx)
80
81 // once the pointer to the request won't change
82 // anymore, finish setting up the replacer
83 addHTTPVarsToReplacer(repl, r, w)
84
82 - // build and execute the main handler chain
85 + loggableReq := LoggableHTTPRequest{r}
86 + errLog := s.errorLogger.With(
87 + // encode the request for logging purposes before
88 + // it enters any handler chain; this is necessary
89 + // to capture the original request in case it gets
90 + // modified during handling
91 + zap.Object("request", loggableReq),
92 + )
93 +
94 + if s.Logs != nil {
95 + wrec := NewResponseRecorder(w, nil, nil)
96 + w = wrec
97 + accLog := s.accessLogger.With(
98 + // capture the original version of the request
99 + zap.Object("request", loggableReq),
100 + )
101 + start := time.Now()
102 + defer func() {
103 + latency := time.Since(start)
104 +
105 + repl.Set("http.response.status", strconv.Itoa(wrec.Status()))
106 + repl.Set("http.response.size", strconv.Itoa(wrec.Size()))
107 + repl.Set("http.response.latency", latency.String())
108 +
109 + logger := accLog
110 + if s.Logs.LoggerNames != nil {
111 + logger = logger.Named(s.Logs.LoggerNames[r.Host])
112 + }
113 +
114 + log := logger.Info
115 + if wrec.Status() >= 400 {
116 + log = logger.Error
117 + }
118 +
119 + log("request",
120 + zap.String("common_log", repl.ReplaceAll(CommonLogFormat, "-")),
121 + zap.Duration("latency", latency),
122 + zap.Int("size", wrec.Size()),
123 + zap.Int("status", wrec.Status()),
124 + )
125 + }()
126 + }
127 +
128 + // guarantee ACME HTTP challenges; handle them
129 + // separately from any user-defined handlers
130 + if s.tlsApp.HandleHTTPChallenge(w, r) {
131 + return
132 + }
133 +
134 + // build and execute the primary handler chain
135 err := s.executeCompositeRoute(w, r, s.Routes)
136 if err != nil {
85 - // add the raw error value to the request context
86 - // so it can be accessed by error handlers
87 - c := context.WithValue(r.Context(), ErrorCtxKey, err)
88 - r = r.WithContext(c)
89 -
90 - // add error values to the replacer
91 - repl.Set("http.error", err.Error())
92 - if handlerErr, ok := err.(HandlerError); ok {
93 - repl.Set("http.error.status_code", strconv.Itoa(handlerErr.StatusCode))
94 - repl.Set("http.error.status_text", http.StatusText(handlerErr.StatusCode))
95 - repl.Set("http.error.message", handlerErr.Message)
96 - repl.Set("http.error.trace", handlerErr.Trace)
97 - repl.Set("http.error.id", handlerErr.ID)
137 + // prepare the error log
138 + logger := errLog
139 + if s.Logs.LoggerNames != nil {
140 + logger = logger.Named(s.Logs.LoggerNames[r.Host])
141 }
142
143 + // get the values that will be used to log the error
144 + errStatus, errMsg, errFields := errLogValues(err)
145 +
146 + // add HTTP error information to request context
147 + r = s.Errors.WithError(r, err)
148 +
149 if s.Errors != nil && len(s.Errors.Routes) > 0 {
101 - err := s.executeCompositeRoute(w, r, s.Errors.Routes)
102 - if err != nil {
103 - // TODO: what should we do if the error handler has an error?
104 - log.Printf("[ERROR] [%s %s] handling error: %v", r.Method, r.RequestURI, err)
105 - }
106 - } else {
107 - // TODO: polish the default error handling
108 - log.Printf("[ERROR] [%s %s] %v", r.Method, r.RequestURI, err)
109 - if handlerErr, ok := err.(HandlerError); ok {
110 - w.WriteHeader(handlerErr.StatusCode)
150 + // execute user-defined error handling route
151 + err2 := s.executeCompositeRoute(w, r, s.Errors.Routes)
152 + if err2 == nil {
153 + // user's error route handled the error response
154 + // successfully, so now just log the error
155 + logger.Error(errMsg, errFields...)
156 } else {
112 - w.WriteHeader(http.StatusInternalServerError)
157 + // well... this is awkward
158 + errFields = append([]zapcore.Field{
159 + zap.String("error", err2.Error()),
160 + zap.Namespace("first_error"),
161 + zap.String("msg", errMsg),
162 + }, errFields...)
163 + logger.Error("error handling handler error", errFields...)
164 }
165 + } else {
166 + logger.Error(errMsg, errFields...)
167 + w.WriteHeader(errStatus)
168 }
169 }
170 }
@@ -278,26 +332,101 @@ type HTTPErrorConfig struct {
332 Routes RouteList `json:"routes,omitempty"`
333 }
334
335 +// WithError makes a shallow copy of r to add the error to its
336 +// context, and sets placeholders on the request's replacer
337 +// related to err. It returns the modified request which has
338 +// the error information in its context and replacer. It
339 +// overwrites any existing error values that are stored.
340 +func (*HTTPErrorConfig) WithError(r *http.Request, err error) *http.Request {
341 + // add the raw error value to the request context
342 + // so it can be accessed by error handlers
343 + c := context.WithValue(r.Context(), ErrorCtxKey, err)
344 + r = r.WithContext(c)
345 +
346 + // add error values to the replacer
347 + repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
348 + repl.Set("http.error", err.Error())
349 + if handlerErr, ok := err.(HandlerError); ok {
350 + repl.Set("http.error.status_code", strconv.Itoa(handlerErr.StatusCode))
351 + repl.Set("http.error.status_text", http.StatusText(handlerErr.StatusCode))
352 + repl.Set("http.error.message", handlerErr.Message)
353 + repl.Set("http.error.trace", handlerErr.Trace)
354 + repl.Set("http.error.id", handlerErr.ID)
355 + }
356 +
357 + return r
358 +}
359 +
360 +// ServerLogConfig describes a server's logging configuration.
361 +type ServerLogConfig struct {
362 + LoggerNames map[string]string `json:"logger_names,omitempty"`
363 +}
364 +
365 +// errLogValues inspects err and returns the status code
366 +// to use, the error log message, and any extra fields.
367 +// If err is a HandlerError, the returned values will
368 +// have richer information.
369 +func errLogValues(err error) (status int, msg string, fields []zapcore.Field) {
370 + if handlerErr, ok := err.(HandlerError); ok {
371 + status = handlerErr.StatusCode
372 + msg = handlerErr.Err.Error()
373 + fields = []zapcore.Field{
374 + zap.Int("status", handlerErr.StatusCode),
375 + zap.String("err_id", handlerErr.ID),
376 + zap.String("err_trace", handlerErr.Trace),
377 + }
378 + return
379 + }
380 + status = http.StatusInternalServerError
381 + msg = err.Error()
382 + return
383 +}
384 +
385 +// originalRequest returns a partial, shallow copy of
386 +// req, including: req.Method, deep copy of req.URL
387 +// (into the urlCopy parameter, which should be on the
388 +// stack), and req.RequestURI. Notably, headers are not
389 +// copied. This function is designed to be very fast
390 +// and efficient, and useful primarly for read-only
391 +// logging purposes.
392 +func originalRequest(req *http.Request, urlCopy *url.URL) http.Request {
393 + urlCopy = cloneURL(req.URL)
394 + return http.Request{
395 + Method: req.Method,
396 + RequestURI: req.RequestURI,
397 + URL: urlCopy,
398 + }
399 +}
400 +
401 // cloneURL makes a copy of r.URL and returns a
402 // new value that doesn't reference the original.
283 -func cloneURL(u *url.URL) url.URL {
403 +func cloneURL(u *url.URL) *url.URL {
404 urlCopy := *u
405 if u.User != nil {
406 userInfo := new(url.Userinfo)
407 *userInfo = *u.User
408 urlCopy.User = userInfo
409 }
290 - return urlCopy
410 + return &urlCopy
411 }
412
413 +const (
414 + // CommonLogFormat is the common log format. https://en.wikipedia.org/wiki/Common_Log_Format
415 + CommonLogFormat = `{http.request.remote.host} ` + CommonLogEmptyValue + ` {http.handlers.authentication.user.id} [{time.now.common_log}] "{http.request.orig_method} {http.request.orig_uri} {http.request.proto}" {http.response.status} {http.response.size}`
416 +
417 + // CommonLogEmptyValue is the common empty log value.
418 + CommonLogEmptyValue = "-"
419 +)
420 +
421 // Context keys for HTTP request context values.
422 const (
423 // For referencing the server instance
424 ServerCtxKey caddy.CtxKey = "server"
425
426 // For the request's variable table
299 - VarCtxKey caddy.CtxKey = "vars"
427 + VarsCtxKey caddy.CtxKey = "vars"
428
301 - // For the unmodified URL that originally came in with a request
302 - OriginalURLCtxKey caddy.CtxKey = "original_url"
429 + // For a partial copy of the unmodified request that
430 + // originally came into the server's entry handler
431 + OriginalRequestCtxKey caddy.CtxKey = "original_request"
432 )
modules/caddyhttp/staticresp.go
+1 -1
@@ -112,7 +112,7 @@ func (s StaticResponse) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Hand
112
113 // write response body
114 if s.Body != "" {
115 - fmt.Fprint(w, repl.ReplaceAll(s.Body, ""))
115 + fmt.Fprint(w, repl.ReplaceKnown(s.Body, ""))
116 }
117
118 return nil
modules/caddyhttp/subroute.go
+22 -3
@@ -30,8 +30,15 @@ func init() {
30 // matchers, or for routes with matchers that must be have deferred
31 // evaluation (e.g. if they depend on placeholders created by other
32 // matchers that need to be evaluated first).
33 +//
34 +// You can also use subroutes to handle errors from specific handlers.
35 +// First the primary Routes will be executed, and if they return an
36 +// error, the Errors routes will be executed; in that case, an error
37 +// is only returned to the entry point at the server if there is an
38 +// additional error returned from the errors routes.
39 type Subroute struct {
34 - Routes RouteList `json:"routes,omitempty"`
40 + Routes RouteList `json:"routes,omitempty"`
41 + Errors *HTTPErrorConfig `json:"errors,omitempty"`
42 }
43
44 // CaddyModule returns the Caddy module information.
@@ -47,7 +54,13 @@ func (sr *Subroute) Provision(ctx caddy.Context) error {
54 if sr.Routes != nil {
55 err := sr.Routes.Provision(ctx)
56 if err != nil {
50 - return fmt.Errorf("setting up routes: %v", err)
57 + return fmt.Errorf("setting up subroutes: %v", err)
58 + }
59 + if sr.Errors != nil {
60 + err := sr.Errors.Routes.Provision(ctx)
61 + if err != nil {
62 + return fmt.Errorf("setting up error subroutes: %v", err)
63 + }
64 }
65 }
66 return nil
@@ -55,7 +68,13 @@ func (sr *Subroute) Provision(ctx caddy.Context) error {
68
69 func (sr *Subroute) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Handler) error {
70 subroute := sr.Routes.BuildCompositeRoute(r)
58 - return subroute.ServeHTTP(w, r)
71 + err := subroute.ServeHTTP(w, r)
72 + if err != nil && sr.Errors != nil {
73 + r = sr.Errors.WithError(r, err)
74 + errRoute := sr.Errors.Routes.BuildCompositeRoute(r)
75 + return errRoute.ServeHTTP(w, r)
76 + }
77 + return err
78 }
79
80 // Interface guards
modules/caddyhttp/vars.go
+2 -2
@@ -38,7 +38,7 @@ func (VarsMiddleware) CaddyModule() caddy.ModuleInfo {
38 }
39
40 func (t VarsMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next Handler) error {
41 - vars := r.Context().Value(VarCtxKey).(map[string]interface{})
41 + vars := r.Context().Value(VarsCtxKey).(map[string]interface{})
42 repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
43 for k, v := range t {
44 keyExpanded := repl.ReplaceAll(k, "")
@@ -62,7 +62,7 @@ func (VarsMatcher) CaddyModule() caddy.ModuleInfo {
62
63 // Match matches a request based on variables in the context.
64 func (m VarsMatcher) Match(r *http.Request) bool {
65 - vars := r.Context().Value(VarCtxKey).(map[string]string)
65 + vars := r.Context().Value(VarsCtxKey).(map[string]string)
66 repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
67 for k, v := range m {
68 keyExpanded := repl.ReplaceAll(k, "")
modules/caddytls/tls.go
+4 -2
@@ -18,7 +18,6 @@ import (
18 "crypto/tls"
19 "encoding/json"
20 "fmt"
21 - "log"
21 "net/http"
22 "sync"
23 "time"
@@ -26,6 +25,7 @@ import (
25 "github.com/caddyserver/caddy/v2"
26 "github.com/go-acme/lego/v3/challenge"
27 "github.com/mholt/certmagic"
28 + "go.uber.org/zap"
29 )
30
31 func init() {
@@ -43,6 +43,7 @@ type TLS struct {
43 ctx caddy.Context
44 storageCleanTicker *time.Ticker
45 storageCleanStop chan struct{}
46 + logger *zap.Logger
47 }
48
49 // CaddyModule returns the Caddy module information.
@@ -56,6 +57,7 @@ func (TLS) CaddyModule() caddy.ModuleInfo {
57 // Provision sets up the configuration for the TLS app.
58 func (t *TLS) Provision(ctx caddy.Context) error {
59 t.ctx = ctx
60 + t.logger = ctx.Logger(t)
61
62 // set up a new certificate cache; this (re)loads all certificates
63 cacheOpts := certmagic.CacheOptions{
@@ -290,7 +292,7 @@ func (t *TLS) cleanStorageUnits() {
292
293 storageClean = time.Now()
294
293 - log.Println("[INFO] tls: Cleaned up storage unit(s)")
295 + t.logger.Info("cleaned up storage units")
296 }
297
298 // CertificateLoader is a type that can load certificates.
modules/logging/encoders.go new
+268
@@ -0,0 +1,268 @@
1 +// Copyright 2015 Matthew Holt and The Caddy Authors
2 +//
3 +// Licensed under the Apache License, Version 2.0 (the "License");
4 +// you may not use this file except in compliance with the License.
5 +// You may obtain a copy of the License at
6 +//
7 +// http://www.apache.org/licenses/LICENSE-2.0
8 +//
9 +// Unless required by applicable law or agreed to in writing, software
10 +// distributed under the License is distributed on an "AS IS" BASIS,
11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 +// See the License for the specific language governing permissions and
13 +// limitations under the License.
14 +
15 +package logging
16 +
17 +import (
18 + "encoding/json"
19 + "fmt"
20 + "strings"
21 + "time"
22 +
23 + "github.com/caddyserver/caddy/v2"
24 + zaplogfmt "github.com/jsternberg/zap-logfmt"
25 + "go.uber.org/zap"
26 + "go.uber.org/zap/buffer"
27 + "go.uber.org/zap/zapcore"
28 +)
29 +
30 +func init() {
31 + caddy.RegisterModule(ConsoleEncoder{})
32 + caddy.RegisterModule(JSONEncoder{})
33 + caddy.RegisterModule(LogfmtEncoder{})
34 + caddy.RegisterModule(StringEncoder{})
35 +}
36 +
37 +// ConsoleEncoder encodes log entries that are mostly human-readable.
38 +type ConsoleEncoder struct {
39 + zapcore.Encoder
40 + LogEncoderConfig
41 +}
42 +
43 +// CaddyModule returns the Caddy module information.
44 +func (ConsoleEncoder) CaddyModule() caddy.ModuleInfo {
45 + return caddy.ModuleInfo{
46 + Name: "caddy.logging.encoders.console",
47 + New: func() caddy.Module { return new(ConsoleEncoder) },
48 + }
49 +}
50 +
51 +// Provision sets up the encoder.
52 +func (ce *ConsoleEncoder) Provision(_ caddy.Context) error {
53 + ce.Encoder = zapcore.NewConsoleEncoder(ce.ZapcoreEncoderConfig())
54 + return nil
55 +}
56 +
57 +// JSONEncoder encodes entries as JSON.
58 +type JSONEncoder struct {
59 + zapcore.Encoder
60 + *LogEncoderConfig
61 +}
62 +
63 +// CaddyModule returns the Caddy module information.
64 +func (JSONEncoder) CaddyModule() caddy.ModuleInfo {
65 + return caddy.ModuleInfo{
66 + Name: "caddy.logging.encoders.json",
67 + New: func() caddy.Module { return new(JSONEncoder) },
68 + }
69 +}
70 +
71 +// Provision sets up the encoder.
72 +func (je *JSONEncoder) Provision(_ caddy.Context) error {
73 + je.Encoder = zapcore.NewJSONEncoder(je.ZapcoreEncoderConfig())
74 + return nil
75 +}
76 +
77 +// LogfmtEncoder encodes log entries as logfmt:
78 +// https://www.brandur.org/logfmt
79 +type LogfmtEncoder struct {
80 + zapcore.Encoder
81 + LogEncoderConfig
82 +}
83 +
84 +// CaddyModule returns the Caddy module information.
85 +func (LogfmtEncoder) CaddyModule() caddy.ModuleInfo {
86 + return caddy.ModuleInfo{
87 + Name: "caddy.logging.encoders.logfmt",
88 + New: func() caddy.Module { return new(LogfmtEncoder) },
89 + }
90 +}
91 +
92 +// Provision sets up the encoder.
93 +func (lfe *LogfmtEncoder) Provision(_ caddy.Context) error {
94 + lfe.Encoder = zaplogfmt.NewEncoder(lfe.ZapcoreEncoderConfig())
95 + return nil
96 +}
97 +
98 +// StringEncoder writes a log entry that consists entirely
99 +// of a single string field in the log entry. This is useful
100 +// for custom, self-encoded log entries that consist of a
101 +// single field in the structured log entry.
102 +type StringEncoder struct {
103 + zapcore.Encoder
104 + FieldName string `json:"field,omitempty"`
105 + FallbackRaw json.RawMessage `json:"fallback,omitempty"`
106 +}
107 +
108 +// CaddyModule returns the Caddy module information.
109 +func (StringEncoder) CaddyModule() caddy.ModuleInfo {
110 + return caddy.ModuleInfo{
111 + Name: "caddy.logging.encoders.string",
112 + New: func() caddy.Module { return new(StringEncoder) },
113 + }
114 +}
115 +
116 +// Provision sets up the encoder.
117 +func (se *StringEncoder) Provision(ctx caddy.Context) error {
118 + if se.FallbackRaw != nil {
119 + val, err := ctx.LoadModuleInline("format", "caddy.logging.encoders", se.FallbackRaw)
120 + if err != nil {
121 + return fmt.Errorf("loading fallback encoder module: %v", err)
122 + }
123 + se.FallbackRaw = nil // allow GC to deallocate
124 + se.Encoder = val.(zapcore.Encoder)
125 + }
126 + if se.Encoder == nil {
127 + se.Encoder = nopEncoder{}
128 + }
129 + return nil
130 +}
131 +
132 +// Clone wraps the underlying encoder's Clone. This is
133 +// necessary because we implement our own EncodeEntry,
134 +// and if we simply let the embedded encoder's Clone
135 +// be promoted, it would return a clone of that, and
136 +// we'd lose our StringEncoder's EncodeEntry.
137 +func (se StringEncoder) Clone() zapcore.Encoder {
138 + return StringEncoder{
139 + Encoder: se.Encoder.Clone(),
140 + FieldName: se.FieldName,
141 + }
142 +}
143 +
144 +// EncodeEntry partially implements the zapcore.Encoder interface.
145 +func (se StringEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
146 + for _, f := range fields {
147 + if f.Key == se.FieldName {
148 + buf := bufferpool.Get()
149 + buf.AppendString(f.String)
150 + if !strings.HasSuffix(f.String, "\n") {
151 + buf.AppendByte('\n')
152 + }
153 + return buf, nil
154 + }
155 + }
156 + if se.Encoder == nil {
157 + return nil, fmt.Errorf("no fallback encoder defined")
158 + }
159 + return se.Encoder.EncodeEntry(ent, fields)
160 +}
161 +
162 +// LogEncoderConfig holds configuration common to most encoders.
163 +type LogEncoderConfig struct {
164 + MessageKey *string `json:"message_key,omitempty"`
165 + LevelKey *string `json:"level_key,omitempty"`
166 + TimeKey *string `json:"time_key,omitempty"`
167 + NameKey *string `json:"name_key,omitempty"`
168 + CallerKey *string `json:"caller_key,omitempty"`
169 + StacktraceKey *string `json:"stacktrace_key,omitempty"`
170 + LineEnding *string `json:"line_ending,omitempty"`
171 + TimeFormat string `json:"time_format,omitempty"`
172 + DurationFormat string `json:"duration_format,omitempty"`
173 + LevelFormat string `json:"level_format,omitempty"`
174 +}
175 +
176 +// ZapcoreEncoderConfig returns the equivalent zapcore.EncoderConfig.
177 +// If lec is nil, zap.NewProductionEncoderConfig() is returned.
178 +func (lec *LogEncoderConfig) ZapcoreEncoderConfig() zapcore.EncoderConfig {
179 + cfg := zap.NewProductionEncoderConfig()
180 + if lec == nil {
181 + lec = new(LogEncoderConfig)
182 + }
183 + if lec.MessageKey != nil {
184 + cfg.MessageKey = *lec.MessageKey
185 + }
186 + if lec.TimeKey != nil {
187 + cfg.TimeKey = *lec.TimeKey
188 + }
189 + if lec.NameKey != nil {
190 + cfg.NameKey = *lec.NameKey
191 + }
192 + if lec.CallerKey != nil {
193 + cfg.CallerKey = *lec.CallerKey
194 + }
195 + if lec.StacktraceKey != nil {
196 + cfg.StacktraceKey = *lec.StacktraceKey
197 + }
198 + if lec.LineEnding != nil {
199 + cfg.LineEnding = *lec.LineEnding
200 + }
201 +
202 + // time format
203 + var timeFormatter zapcore.TimeEncoder
204 + switch lec.TimeFormat {
205 + case "", "unix_seconds_float":
206 + timeFormatter = zapcore.EpochTimeEncoder
207 + case "unix_milli_float":
208 + timeFormatter = zapcore.EpochMillisTimeEncoder
209 + case "unix_nano":
210 + timeFormatter = zapcore.EpochNanosTimeEncoder
211 + case "iso8601":
212 + timeFormatter = zapcore.ISO8601TimeEncoder
213 + default:
214 + timeFormat := lec.TimeFormat
215 + switch lec.TimeFormat {
216 + case "rfc3339":
217 + timeFormat = time.RFC3339
218 + case "rfc3339_nano":
219 + timeFormat = time.RFC3339Nano
220 + case "wall":
221 + timeFormat = "2006/01/02 15:04:05"
222 + case "wall_milli":
223 + timeFormat = "2006/01/02 15:04:05.000"
224 + case "wall_nano":
225 + timeFormat = "2006/01/02 15:04:05.000000000"
226 + }
227 + timeFormatter = func(ts time.Time, encoder zapcore.PrimitiveArrayEncoder) {
228 + encoder.AppendString(ts.UTC().Format(timeFormat))
229 + }
230 + }
231 + cfg.EncodeTime = timeFormatter
232 +
233 + // duration format
234 + var durFormatter zapcore.DurationEncoder
235 + switch lec.DurationFormat {
236 + case "", "seconds":
237 + durFormatter = zapcore.SecondsDurationEncoder
238 + case "nano":
239 + durFormatter = zapcore.NanosDurationEncoder
240 + case "string":
241 + durFormatter = zapcore.StringDurationEncoder
242 + }
243 + cfg.EncodeDuration = durFormatter
244 +
245 + // level format
246 + var levelFormatter zapcore.LevelEncoder
247 + switch lec.LevelFormat {
248 + case "", "lower":
249 + levelFormatter = zapcore.LowercaseLevelEncoder
250 + case "upper":
251 + levelFormatter = zapcore.CapitalLevelEncoder
252 + case "color":
253 + levelFormatter = zapcore.CapitalColorLevelEncoder
254 + }
255 + cfg.EncodeLevel = levelFormatter
256 +
257 + return cfg
258 +}
259 +
260 +var bufferpool = buffer.NewPool()
261 +
262 +// Interface guards
263 +var (
264 + _ zapcore.Encoder = (*ConsoleEncoder)(nil)
265 + _ zapcore.Encoder = (*JSONEncoder)(nil)
266 + _ zapcore.Encoder = (*LogfmtEncoder)(nil)
267 + _ zapcore.Encoder = (*StringEncoder)(nil)
268 +)
modules/logging/filewriter.go new
+91
@@ -0,0 +1,91 @@
1 +// Copyright 2015 Matthew Holt and The Caddy Authors
2 +//
3 +// Licensed under the Apache License, Version 2.0 (the "License");
4 +// you may not use this file except in compliance with the License.
5 +// You may obtain a copy of the License at
6 +//
7 +// http://www.apache.org/licenses/LICENSE-2.0
8 +//
9 +// Unless required by applicable law or agreed to in writing, software
10 +// distributed under the License is distributed on an "AS IS" BASIS,
11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 +// See the License for the specific language governing permissions and
13 +// limitations under the License.
14 +
15 +package logging
16 +
17 +import (
18 + "io"
19 + "os"
20 + "path/filepath"
21 +
22 + "github.com/caddyserver/caddy/v2"
23 + "gopkg.in/natefinch/lumberjack.v2"
24 +)
25 +
26 +func init() {
27 + caddy.RegisterModule(FileWriter{})
28 +}
29 +
30 +// FileWriter can write logs to files.
31 +type FileWriter struct {
32 + Filename string `json:"filename,omitempty"`
33 + Roll *bool `json:"roll,omitempty"`
34 + RollSizeMB int `json:"roll_size_mb,omitempty"`
35 + RollCompress *bool `json:"roll_gzip,omitempty"`
36 + RollLocalTime bool `json:"roll_local_time,omitempty"`
37 + RollKeep int `json:"roll_keep,omitempty"`
38 + RollKeepDays int `json:"roll_keep_days,omitempty"`
39 +}
40 +
41 +// CaddyModule returns the Caddy module information.
42 +func (FileWriter) CaddyModule() caddy.ModuleInfo {
43 + return caddy.ModuleInfo{
44 + Name: "caddy.logging.writers.file",
45 + New: func() caddy.Module { return new(FileWriter) },
46 + }
47 +}
48 +
49 +func (fw FileWriter) String() string {
50 + fpath, err := filepath.Abs(fw.Filename)
51 + if err == nil {
52 + return fpath
53 + }
54 + return fw.Filename
55 +}
56 +
57 +// WriterKey returns a unique key representing this fw.
58 +func (fw FileWriter) WriterKey() string {
59 + return "file:" + fw.Filename
60 +}
61 +
62 +// OpenWriter opens a new file writer.
63 +func (fw FileWriter) OpenWriter() (io.WriteCloser, error) {
64 + // roll log files by default
65 + if fw.Roll == nil || *fw.Roll == true {
66 + if fw.RollSizeMB == 0 {
67 + fw.RollSizeMB = 100
68 + }
69 + if fw.RollCompress == nil {
70 + compress := true
71 + fw.RollCompress = &compress
72 + }
73 + if fw.RollKeep == 0 {
74 + fw.RollKeep = 10
75 + }
76 + if fw.RollKeepDays == 0 {
77 + fw.RollKeepDays = 90
78 + }
79 + return &lumberjack.Logger{
80 + Filename: fw.Filename,
81 + MaxSize: fw.RollSizeMB,
82 + MaxAge: fw.RollKeepDays,
83 + MaxBackups: fw.RollKeep,
84 + LocalTime: fw.RollLocalTime,
85 + Compress: *fw.RollCompress,
86 + }, nil
87 + }
88 +
89 + // otherwise just open a regular file
90 + return os.OpenFile(fw.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
91 +}
modules/logging/filterencoder.go new
+321
@@ -0,0 +1,321 @@
1 +// Copyright 2015 Matthew Holt and The Caddy Authors
2 +//
3 +// Licensed under the Apache License, Version 2.0 (the "License");
4 +// you may not use this file except in compliance with the License.
5 +// You may obtain a copy of the License at
6 +//
7 +// http://www.apache.org/licenses/LICENSE-2.0
8 +//
9 +// Unless required by applicable law or agreed to in writing, software
10 +// distributed under the License is distributed on an "AS IS" BASIS,
11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 +// See the License for the specific language governing permissions and
13 +// limitations under the License.
14 +
15 +package logging
16 +
17 +import (
18 + "encoding/json"
19 + "fmt"
20 + "time"
21 +
22 + "github.com/caddyserver/caddy/v2"
23 + "go.uber.org/zap"
24 + "go.uber.org/zap/buffer"
25 + "go.uber.org/zap/zapcore"
26 +)
27 +
28 +func init() {
29 + caddy.RegisterModule(FilterEncoder{})
30 +}
31 +
32 +// FilterEncoder wraps an underlying encoder. It does
33 +// not do any encoding itself, but it can manipulate
34 +// (filter) fields before they are actually encoded.
35 +// A wrapped encoder is required.
36 +type FilterEncoder struct {
37 + WrappedRaw json.RawMessage `json:"wrap,omitempty"`
38 + FieldsRaw map[string]json.RawMessage `json:"fields,omitempty"`
39 +
40 + wrapped zapcore.Encoder
41 + Fields map[string]LogFieldFilter `json:"-"`
42 +
43 + // used to keep keys unique across nested objects
44 + keyPrefix string
45 +}
46 +
47 +// CaddyModule returns the Caddy module information.
48 +func (FilterEncoder) CaddyModule() caddy.ModuleInfo {
49 + return caddy.ModuleInfo{
50 + Name: "caddy.logging.encoders.filter",
51 + New: func() caddy.Module { return new(FilterEncoder) },
52 + }
53 +}
54 +
55 +// Provision sets up the encoder.
56 +func (fe *FilterEncoder) Provision(ctx caddy.Context) error {
57 + if fe.WrappedRaw == nil {
58 + return fmt.Errorf("missing \"wrap\" (must specify an underlying encoder)")
59 + }
60 +
61 + // set up wrapped encoder (required)
62 + val, err := ctx.LoadModuleInline("format", "caddy.logging.encoders", fe.WrappedRaw)
63 + if err != nil {
64 + return fmt.Errorf("loading fallback encoder module: %v", err)
65 + }
66 + fe.WrappedRaw = nil // allow GC to deallocate
67 + fe.wrapped = val.(zapcore.Encoder)
68 +
69 + // set up each field filter
70 + if fe.Fields == nil {
71 + fe.Fields = make(map[string]LogFieldFilter)
72 + }
73 + for field, filterRaw := range fe.FieldsRaw {
74 + if filterRaw == nil {
75 + continue
76 + }
77 + val, err := ctx.LoadModuleInline("filter", "caddy.logging.encoders.filter", filterRaw)
78 + if err != nil {
79 + return fmt.Errorf("loading log filter module: %v", err)
80 + }
81 + fe.Fields[field] = val.(LogFieldFilter)
82 + }
83 + fe.FieldsRaw = nil // allow GC to deallocate
84 +
85 + return nil
86 +}
87 +
88 +// AddArray is part of the zapcore.ObjectEncoder interface.
89 +// Array elements do not get filtered.
90 +func (fe FilterEncoder) AddArray(key string, marshaler zapcore.ArrayMarshaler) error {
91 + if filter, ok := fe.Fields[fe.keyPrefix+key]; ok {
92 + filter.Filter(zap.Array(key, marshaler)).AddTo(fe.wrapped)
93 + return nil
94 + }
95 + return fe.wrapped.AddArray(key, marshaler)
96 +}
97 +
98 +// AddObject is part of the zapcore.ObjectEncoder interface.
99 +func (fe FilterEncoder) AddObject(key string, marshaler zapcore.ObjectMarshaler) error {
100 + fe.keyPrefix += key + ">"
101 + return fe.wrapped.AddObject(key, logObjectMarshalerWrapper{
102 + enc: fe,
103 + marsh: marshaler,
104 + })
105 +}
106 +
107 +// AddBinary is part of the zapcore.ObjectEncoder interface.
108 +func (fe FilterEncoder) AddBinary(key string, value []byte) {
109 + if !fe.filtered(key, value) {
110 + fe.wrapped.AddBinary(key, value)
111 + }
112 +}
113 +
114 +// AddByteString is part of the zapcore.ObjectEncoder interface.
115 +func (fe FilterEncoder) AddByteString(key string, value []byte) {
116 + if !fe.filtered(key, value) {
117 + fe.wrapped.AddByteString(key, value)
118 + }
119 +}
120 +
121 +// AddBool is part of the zapcore.ObjectEncoder interface.
122 +func (fe FilterEncoder) AddBool(key string, value bool) {
123 + if !fe.filtered(key, value) {
124 + fe.wrapped.AddBool(key, value)
125 + }
126 +}
127 +
128 +// AddComplex128 is part of the zapcore.ObjectEncoder interface.
129 +func (fe FilterEncoder) AddComplex128(key string, value complex128) {
130 + if !fe.filtered(key, value) {
131 + fe.wrapped.AddComplex128(key, value)
132 + }
133 +}
134 +
135 +// AddComplex64 is part of the zapcore.ObjectEncoder interface.
136 +func (fe FilterEncoder) AddComplex64(key string, value complex64) {
137 + if !fe.filtered(key, value) {
138 + fe.wrapped.AddComplex64(key, value)
139 + }
140 +}
141 +
142 +// AddDuration is part of the zapcore.ObjectEncoder interface.
143 +func (fe FilterEncoder) AddDuration(key string, value time.Duration) {
144 + if !fe.filtered(key, value) {
145 + fe.wrapped.AddDuration(key, value)
146 + }
147 +}
148 +
149 +// AddFloat64 is part of the zapcore.ObjectEncoder interface.
150 +func (fe FilterEncoder) AddFloat64(key string, value float64) {
151 + if !fe.filtered(key, value) {
152 + fe.wrapped.AddFloat64(key, value)
153 + }
154 +}
155 +
156 +// AddFloat32 is part of the zapcore.ObjectEncoder interface.
157 +func (fe FilterEncoder) AddFloat32(key string, value float32) {
158 + if !fe.filtered(key, value) {
159 + fe.wrapped.AddFloat32(key, value)
160 + }
161 +}
162 +
163 +// AddInt is part of the zapcore.ObjectEncoder interface.
164 +func (fe FilterEncoder) AddInt(key string, value int) {
165 + if !fe.filtered(key, value) {
166 + fe.wrapped.AddInt(key, value)
167 + }
168 +}
169 +
170 +// AddInt64 is part of the zapcore.ObjectEncoder interface.
171 +func (fe FilterEncoder) AddInt64(key string, value int64) {
172 + if !fe.filtered(key, value) {
173 + fe.wrapped.AddInt64(key, value)
174 + }
175 +}
176 +
177 +// AddInt32 is part of the zapcore.ObjectEncoder interface.
178 +func (fe FilterEncoder) AddInt32(key string, value int32) {
179 + if !fe.filtered(key, value) {
180 + fe.wrapped.AddInt32(key, value)
181 + }
182 +}
183 +
184 +// AddInt16 is part of the zapcore.ObjectEncoder interface.
185 +func (fe FilterEncoder) AddInt16(key string, value int16) {
186 + if !fe.filtered(key, value) {
187 + fe.wrapped.AddInt16(key, value)
188 + }
189 +}
190 +
191 +// AddInt8 is part of the zapcore.ObjectEncoder interface.
192 +func (fe FilterEncoder) AddInt8(key string, value int8) {
193 + if !fe.filtered(key, value) {
194 + fe.wrapped.AddInt8(key, value)
195 + }
196 +}
197 +
198 +// AddString is part of the zapcore.ObjectEncoder interface.
199 +func (fe FilterEncoder) AddString(key, value string) {
200 + if !fe.filtered(key, value) {
201 + fe.wrapped.AddString(key, value)
202 + }
203 +}
204 +
205 +// AddTime is part of the zapcore.ObjectEncoder interface.
206 +func (fe FilterEncoder) AddTime(key string, value time.Time) {
207 + if !fe.filtered(key, value) {
208 + fe.wrapped.AddTime(key, value)
209 + }
210 +}
211 +
212 +// AddUint is part of the zapcore.ObjectEncoder interface.
213 +func (fe FilterEncoder) AddUint(key string, value uint) {
214 + if !fe.filtered(key, value) {
215 + fe.wrapped.AddUint(key, value)
216 + }
217 +}
218 +
219 +// AddUint64 is part of the zapcore.ObjectEncoder interface.
220 +func (fe FilterEncoder) AddUint64(key string, value uint64) {
221 + if !fe.filtered(key, value) {
222 + fe.wrapped.AddUint64(key, value)
223 + }
224 +}
225 +
226 +// AddUint32 is part of the zapcore.ObjectEncoder interface.
227 +func (fe FilterEncoder) AddUint32(key string, value uint32) {
228 + if !fe.filtered(key, value) {
229 + fe.wrapped.AddUint32(key, value)
230 + }
231 +}
232 +
233 +// AddUint16 is part of the zapcore.ObjectEncoder interface.
234 +func (fe FilterEncoder) AddUint16(key string, value uint16) {
235 + if !fe.filtered(key, value) {
236 + fe.wrapped.AddUint16(key, value)
237 + }
238 +}
239 +
240 +// AddUint8 is part of the zapcore.ObjectEncoder interface.
241 +func (fe FilterEncoder) AddUint8(key string, value uint8) {
242 + if !fe.filtered(key, value) {
243 + fe.wrapped.AddUint8(key, value)
244 + }
245 +}
246 +
247 +// AddUintptr is part of the zapcore.ObjectEncoder interface.
248 +func (fe FilterEncoder) AddUintptr(key string, value uintptr) {
249 + if !fe.filtered(key, value) {
250 + fe.wrapped.AddUintptr(key, value)
251 + }
252 +}
253 +
254 +// AddReflected is part of the zapcore.ObjectEncoder interface.
255 +func (fe FilterEncoder) AddReflected(key string, value interface{}) error {
256 + if !fe.filtered(key, value) {
257 + return fe.wrapped.AddReflected(key, value)
258 + }
259 + return nil
260 +}
261 +
262 +// OpenNamespace is part of the zapcore.ObjectEncoder interface.
263 +func (fe FilterEncoder) OpenNamespace(key string) {
264 + fe.wrapped.OpenNamespace(key)
265 +}
266 +
267 +// Clone is part of the zapcore.ObjectEncoder interface.
268 +// We don't use it as of Oct 2019 (v2 beta 7), I'm not
269 +// really sure what it'd be useful for in our case.
270 +func (fe FilterEncoder) Clone() zapcore.Encoder {
271 + return FilterEncoder{
272 + Fields: fe.Fields,
273 + wrapped: fe.wrapped.Clone(),
274 + keyPrefix: fe.keyPrefix,
275 + }
276 +}
277 +
278 +// EncodeEntry partially implements the zapcore.Encoder interface.
279 +func (fe FilterEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
280 + // without this clone and storing it to fe.wrapped, fields
281 + // from subsequent log entries get appended to previous
282 + // ones, and I'm not 100% sure why; see end of
283 + // https://github.com/uber-go/zap/issues/750
284 + fe.wrapped = fe.wrapped.Clone()
285 + for _, field := range fields {
286 + field.AddTo(fe)
287 + }
288 + return fe.wrapped.EncodeEntry(ent, nil)
289 +}
290 +
291 +// filtered returns true if the field was filtered.
292 +// If true is returned, the field was filtered and
293 +// added to the underlying encoder (so do not do
294 +// that again). If false was returned, the field has
295 +// not yet been added to the underlying encoder.
296 +func (fe FilterEncoder) filtered(key string, value interface{}) bool {
297 + filter, ok := fe.Fields[fe.keyPrefix+key]
298 + if !ok {
299 + return false
300 + }
301 + filter.Filter(zap.Any(key, value)).AddTo(fe.wrapped)
302 + return true
303 +}
304 +
305 +// logObjectMarshalerWrapper allows us to recursively
306 +// filter fields of objects as they get encoded.
307 +type logObjectMarshalerWrapper struct {
308 + enc FilterEncoder
309 + marsh zapcore.ObjectMarshaler
310 +}
311 +
312 +// MarshalLogObject implements the zapcore.ObjectMarshaler interface.
313 +func (mom logObjectMarshalerWrapper) MarshalLogObject(_ zapcore.ObjectEncoder) error {
314 + return mom.marsh.MarshalLogObject(mom.enc)
315 +}
316 +
317 +// Interface guards
318 +var (
319 + _ zapcore.Encoder = (*FilterEncoder)(nil)
320 + _ zapcore.ObjectMarshaler = (*logObjectMarshalerWrapper)(nil)
321 +)
modules/logging/filters.go new
+94
@@ -0,0 +1,94 @@
1 +// Copyright 2015 Matthew Holt and The Caddy Authors
2 +//
3 +// Licensed under the Apache License, Version 2.0 (the "License");
4 +// you may not use this file except in compliance with the License.
5 +// You may obtain a copy of the License at
6 +//
7 +// http://www.apache.org/licenses/LICENSE-2.0
8 +//
9 +// Unless required by applicable law or agreed to in writing, software
10 +// distributed under the License is distributed on an "AS IS" BASIS,
11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 +// See the License for the specific language governing permissions and
13 +// limitations under the License.
14 +
15 +package logging
16 +
17 +import (
18 + "net"
19 +
20 + "github.com/caddyserver/caddy/v2"
21 + "go.uber.org/zap/zapcore"
22 +)
23 +
24 +func init() {
25 + caddy.RegisterModule(DeleteFilter{})
26 + caddy.RegisterModule(IPMaskFilter{})
27 +}
28 +
29 +// LogFieldFilter can filter (or manipulate)
30 +// a field in a log entry. If delete is true,
31 +// out will be ignored and the field will be
32 +// removed from the output.
33 +type LogFieldFilter interface {
34 + Filter(zapcore.Field) zapcore.Field
35 +}
36 +
37 +// DeleteFilter is a Caddy log field filter that
38 +// deletes the field.
39 +type DeleteFilter struct{}
40 +
41 +// CaddyModule returns the Caddy module information.
42 +func (DeleteFilter) CaddyModule() caddy.ModuleInfo {
43 + return caddy.ModuleInfo{
44 + Name: "caddy.logging.encoders.filter.delete",
45 + New: func() caddy.Module { return new(DeleteFilter) },
46 + }
47 +}
48 +
49 +// Filter filters the input field.
50 +func (DeleteFilter) Filter(in zapcore.Field) zapcore.Field {
51 + in.Type = zapcore.SkipType
52 + return in
53 +}
54 +
55 +// IPMaskFilter is a Caddy log field filter that
56 +// masks IP addresses.
57 +type IPMaskFilter struct {
58 + IPv4CIDR int `json:"ipv4_cidr,omitempty"`
59 + IPv6CIDR int `json:"ipv6_cidr,omitempty"`
60 +}
61 +
62 +// CaddyModule returns the Caddy module information.
63 +func (IPMaskFilter) CaddyModule() caddy.ModuleInfo {
64 + return caddy.ModuleInfo{
65 + Name: "caddy.logging.encoders.filter.ip_mask",
66 + New: func() caddy.Module { return new(IPMaskFilter) },
67 + }
68 +}
69 +
70 +// Filter filters the input field.
71 +func (m IPMaskFilter) Filter(in zapcore.Field) zapcore.Field {
72 + host, port, err := net.SplitHostPort(in.String)
73 + if err != nil {
74 + host = in.String // assume whole thing was IP address
75 + }
76 + ipAddr := net.ParseIP(host)
77 + if ipAddr == nil {
78 + return in
79 + }
80 + bitLen := 32
81 + cidrPrefix := m.IPv4CIDR
82 + if ipAddr.To16() != nil {
83 + bitLen = 128
84 + cidrPrefix = m.IPv6CIDR
85 + }
86 + mask := net.CIDRMask(cidrPrefix, bitLen)
87 + masked := ipAddr.Mask(mask)
88 + if port == "" {
89 + in.String = masked.String()
90 + } else {
91 + in.String = net.JoinHostPort(masked.String(), port)
92 + }
93 + return in
94 +}
modules/logging/nopencoder.go new
+114
@@ -0,0 +1,114 @@
1 +// Copyright 2015 Matthew Holt and The Caddy Authors
2 +//
3 +// Licensed under the Apache License, Version 2.0 (the "License");
4 +// you may not use this file except in compliance with the License.
5 +// You may obtain a copy of the License at
6 +//
7 +// http://www.apache.org/licenses/LICENSE-2.0
8 +//
9 +// Unless required by applicable law or agreed to in writing, software
10 +// distributed under the License is distributed on an "AS IS" BASIS,
11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 +// See the License for the specific language governing permissions and
13 +// limitations under the License.
14 +
15 +package logging
16 +
17 +import (
18 + "time"
19 +
20 + "go.uber.org/zap/buffer"
21 + "go.uber.org/zap/zapcore"
22 +)
23 +
24 +// nopEncoder is a zapcore.Encoder that does nothing.
25 +type nopEncoder struct{}
26 +
27 +// AddArray is part of the zapcore.ObjectEncoder interface.
28 +// Array elements do not get filtered.
29 +func (nopEncoder) AddArray(key string, marshaler zapcore.ArrayMarshaler) error { return nil }
30 +
31 +// AddObject is part of the zapcore.ObjectEncoder interface.
32 +func (nopEncoder) AddObject(key string, marshaler zapcore.ObjectMarshaler) error { return nil }
33 +
34 +// AddBinary is part of the zapcore.ObjectEncoder interface.
35 +func (nopEncoder) AddBinary(key string, value []byte) {}
36 +
37 +// AddByteString is part of the zapcore.ObjectEncoder interface.
38 +func (nopEncoder) AddByteString(key string, value []byte) {}
39 +
40 +// AddBool is part of the zapcore.ObjectEncoder interface.
41 +func (nopEncoder) AddBool(key string, value bool) {}
42 +
43 +// AddComplex128 is part of the zapcore.ObjectEncoder interface.
44 +func (nopEncoder) AddComplex128(key string, value complex128) {}
45 +
46 +// AddComplex64 is part of the zapcore.ObjectEncoder interface.
47 +func (nopEncoder) AddComplex64(key string, value complex64) {}
48 +
49 +// AddDuration is part of the zapcore.ObjectEncoder interface.
50 +func (nopEncoder) AddDuration(key string, value time.Duration) {}
51 +
52 +// AddFloat64 is part of the zapcore.ObjectEncoder interface.
53 +func (nopEncoder) AddFloat64(key string, value float64) {}
54 +
55 +// AddFloat32 is part of the zapcore.ObjectEncoder interface.
56 +func (nopEncoder) AddFloat32(key string, value float32) {}
57 +
58 +// AddInt is part of the zapcore.ObjectEncoder interface.
59 +func (nopEncoder) AddInt(key string, value int) {}
60 +
61 +// AddInt64 is part of the zapcore.ObjectEncoder interface.
62 +func (nopEncoder) AddInt64(key string, value int64) {}
63 +
64 +// AddInt32 is part of the zapcore.ObjectEncoder interface.
65 +func (nopEncoder) AddInt32(key string, value int32) {}
66 +
67 +// AddInt16 is part of the zapcore.ObjectEncoder interface.
68 +func (nopEncoder) AddInt16(key string, value int16) {}
69 +
70 +// AddInt8 is part of the zapcore.ObjectEncoder interface.
71 +func (nopEncoder) AddInt8(key string, value int8) {}
72 +
73 +// AddString is part of the zapcore.ObjectEncoder interface.
74 +func (nopEncoder) AddString(key, value string) {}
75 +
76 +// AddTime is part of the zapcore.ObjectEncoder interface.
77 +func (nopEncoder) AddTime(key string, value time.Time) {}
78 +
79 +// AddUint is part of the zapcore.ObjectEncoder interface.
80 +func (nopEncoder) AddUint(key string, value uint) {}
81 +
82 +// AddUint64 is part of the zapcore.ObjectEncoder interface.
83 +func (nopEncoder) AddUint64(key string, value uint64) {}
84 +
85 +// AddUint32 is part of the zapcore.ObjectEncoder interface.
86 +func (nopEncoder) AddUint32(key string, value uint32) {}
87 +
88 +// AddUint16 is part of the zapcore.ObjectEncoder interface.
89 +func (nopEncoder) AddUint16(key string, value uint16) {}
90 +
91 +// AddUint8 is part of the zapcore.ObjectEncoder interface.
92 +func (nopEncoder) AddUint8(key string, value uint8) {}
93 +
94 +// AddUintptr is part of the zapcore.ObjectEncoder interface.
95 +func (nopEncoder) AddUintptr(key string, value uintptr) {}
96 +
97 +// AddReflected is part of the zapcore.ObjectEncoder interface.
98 +func (nopEncoder) AddReflected(key string, value interface{}) error { return nil }
99 +
100 +// OpenNamespace is part of the zapcore.ObjectEncoder interface.
101 +func (nopEncoder) OpenNamespace(key string) {}
102 +
103 +// Clone is part of the zapcore.ObjectEncoder interface.
104 +// We don't use it as of Oct 2019 (v2 beta 7), I'm not
105 +// really sure what it'd be useful for in our case.
106 +func (ne nopEncoder) Clone() zapcore.Encoder { return ne }
107 +
108 +// EncodeEntry partially implements the zapcore.Encoder interface.
109 +func (nopEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
110 + return bufferpool.Get(), nil
111 +}
112 +
113 +// Interface guard
114 +var _ zapcore.Encoder = (*nopEncoder)(nil)
replacer.go
+53 -14
@@ -15,6 +15,7 @@
15 package caddy
16
17 import (
18 + "fmt"
19 "os"
20 "path/filepath"
21 "runtime"
@@ -28,6 +29,8 @@ type Replacer interface {
29 Delete(variable string)
30 Map(ReplacementFunc)
31 ReplaceAll(input, empty string) string
32 + ReplaceKnown(input, empty string) string
33 + ReplaceOrErr(input string, errOnEmpty, errOnUnknown bool) (string, error)
34 }
35
36 // NewReplacer returns a new Replacer.
@@ -70,12 +73,34 @@ func (r *replacer) fromStatic(key string) (val string, ok bool) {
73 return
74 }
75
76 +// ReplaceOrErr is like ReplaceAll, but any placeholders
77 +// that are empty or not recognized will cause an error to
78 +// be returned.
79 +func (r *replacer) ReplaceOrErr(input string, errOnEmpty, errOnUnknown bool) (string, error) {
80 + return r.replace(input, "", false, errOnEmpty, errOnUnknown)
81 +}
82 +
83 +// ReplaceKnown is like ReplaceAll but only replaces
84 +// placeholders that are known (recognized). Unrecognized
85 +// placeholders will remain in the output.
86 +func (r *replacer) ReplaceKnown(input, empty string) string {
87 + out, _ := r.replace(input, empty, false, false, false)
88 + return out
89 +}
90 +
91 // ReplaceAll efficiently replaces placeholders in input with
74 -// their values. Unrecognized placeholders will not be replaced.
75 -// Values that are empty string will be substituted with empty.
92 +// their values. All placeholders are replaced in the output
93 +// whether they are recognized or not. Values that are empty
94 +// string will be substituted with empty.
95 func (r *replacer) ReplaceAll(input, empty string) string {
96 + out, _ := r.replace(input, empty, true, false, false)
97 + return out
98 +}
99 +
100 +func (r *replacer) replace(input, empty string,
101 + treatUnknownAsEmpty, errOnEmpty, errOnUnknown bool) (string, error) {
102 if !strings.Contains(input, string(phOpen)) {
78 - return input
103 + return input, nil
104 }
105
106 var sb strings.Builder
@@ -100,24 +125,39 @@ func (r *replacer) ReplaceAll(input, empty string) string {
125 // trim opening bracket
126 key := input[i+1 : end]
127
103 - // try to get a value for this key; if
104 - // the key is not recognized, do not
105 - // perform any replacement
128 + // try to get a value for this key,
129 + // handle empty values accordingly
130 var found bool
131 for _, mapFunc := range r.providers {
132 if val, ok := mapFunc(key); ok {
133 found = true
110 - if val != "" {
134 + if val == "" {
135 + if errOnEmpty {
136 + return "", fmt.Errorf("evaluated placeholder %s%s%s is empty",
137 + string(phOpen), key, string(phClose))
138 + } else if empty != "" {
139 + sb.WriteString(empty)
140 + }
141 + } else {
142 sb.WriteString(val)
112 - } else if empty != "" {
113 - sb.WriteString(empty)
143 }
144 break
145 }
146 }
147 if !found {
119 - lastWriteCursor = i
120 - continue
148 + // placeholder is unknown (unrecognized), handle accordingly
149 + switch {
150 + case errOnUnknown:
151 + return "", fmt.Errorf("unrecognized placeholder %s%s%s",
152 + string(phOpen), key, string(phClose))
153 + case treatUnknownAsEmpty:
154 + if empty != "" {
155 + sb.WriteString(empty)
156 + }
157 + default:
158 + lastWriteCursor = i
159 + continue
160 + }
161 }
162
163 // advance cursor to end of placeholder
@@ -128,7 +168,7 @@ func (r *replacer) ReplaceAll(input, empty string) string {
168 // flush any unwritten remainder
169 sb.WriteString(input[lastWriteCursor:])
170
131 - return sb.String()
171 + return sb.String(), nil
172 }
173
174 // ReplacementFunc is a function that returns a replacement
@@ -142,8 +182,7 @@ func globalDefaultReplacements(key string) (string, bool) {
182 // check environment variable
183 const envPrefix = "env."
184 if strings.HasPrefix(key, envPrefix) {
145 - val := os.Getenv(key[len(envPrefix):])
146 - return val, val != ""
185 + return os.Getenv(key[len(envPrefix):]), true
186 }
187
188 switch key {
replacer_test.go
+2 -2
@@ -85,7 +85,7 @@ func TestReplacerSet(t *testing.T) {
85 }
86 }
87
88 -func TestReplacerReplaceAll(t *testing.T) {
88 +func TestReplacerReplaceKnown(t *testing.T) {
89 rep := replacer{
90 providers: []ReplacementFunc{
91 // split our possible vars to two functions (to test if both functions are called)
@@ -148,7 +148,7 @@ func TestReplacerReplaceAll(t *testing.T) {
148 expected: "val1 {nope} test-123 ",
149 },
150 } {
151 - actual := rep.ReplaceAll(tc.testInput, "EMPTY")
151 + actual := rep.ReplaceKnown(tc.testInput, "EMPTY")
152
153 // test if all are replaced as expected
154 if actual != tc.expected {
sigtrap.go
+9 -5
@@ -15,9 +15,10 @@
15 package caddy
16
17 import (
18 - "log"
18 "os"
19 "os/signal"
20 +
21 + "go.uber.org/zap"
22 )
23
24 // TrapSignals create signal/interrupt handlers as best it can for the
@@ -41,11 +42,11 @@ func trapSignalsCrossPlatform() {
42 <-shutdown
43
44 if i > 0 {
44 - log.Println("[INFO] SIGINT: Force quit")
45 + Log().Warn("force quit", zap.String("signal", "SIGINT"))
46 os.Exit(ExitCodeForceQuit)
47 }
48
48 - log.Println("[INFO] SIGINT: Shutting down")
49 + Log().Info("shutting down", zap.String("signal", "SIGINT"))
50 go gracefulStop("SIGINT")
51 }
52 }()
@@ -57,11 +58,14 @@ func gracefulStop(sigName string) {
58
59 err := stopAndCleanup()
60 if err != nil {
60 - log.Printf("[ERROR] %s stop: %v", sigName, err)
61 + Log().Error("stopping",
62 + zap.String("signal", sigName),
63 + zap.Error(err),
64 + )
65 exitCode = ExitCodeFailedQuit
66 }
67
64 - log.Printf("[INFO] %s: Shutdown done", sigName)
68 + Log().Info("shutdown done", zap.String("signal", sigName))
69 os.Exit(exitCode)
70 }
71
sigtrap_posix.go
+6 -6
@@ -17,12 +17,12 @@
17 package caddy
18
19 import (
20 - "log"
20 "os"
21 "os/signal"
22 "syscall"
23
24 "github.com/mholt/certmagic"
25 + "go.uber.org/zap"
26 )
27
28 // trapSignalsPosix captures POSIX-only signals.
@@ -34,23 +34,23 @@ func trapSignalsPosix() {
34 for sig := range sigchan {
35 switch sig {
36 case syscall.SIGQUIT:
37 - log.Println("[INFO] SIGQUIT: Quitting process immediately")
37 + Log().Info("quitting process immediately", zap.String("signal", "SIGQUIT"))
38 certmagic.CleanUpOwnLocks() // try to clean up locks anyway, it's important
39 os.Exit(ExitCodeForceQuit)
40
41 case syscall.SIGTERM:
42 - log.Println("[INFO] SIGTERM: Shutting down apps then terminating")
42 + Log().Info("shutting down apps then terminating", zap.String("signal", "SIGTERM"))
43 gracefulStop("SIGTERM")
44
45 case syscall.SIGUSR1:
46 - log.Println("[INFO] SIGUSR1: Not implemented")
46 + Log().Info("not implemented", zap.String("signal", "SIGUSR1"))
47
48 case syscall.SIGUSR2:
49 - log.Println("[INFO] SIGUSR2: Not implemented")
49 + Log().Info("not implemented", zap.String("signal", "SIGUSR2"))
50
51 case syscall.SIGHUP:
52 // ignore; this signal is sometimes sent outside of the user's control
53 - log.Println("[INFO] SIGHUP: Not implemented")
53 + Log().Info("not implemented", zap.String("signal", "SIGHUP"))
54 }
55 }
56 }()
usagepool.go
+157 -47
@@ -21,23 +21,136 @@ import (
21 )
22
23 // UsagePool is a thread-safe map that pools values
24 -// based on usage; a LoadOrStore operation increments
25 -// the usage, and a Delete decrements from the usage.
26 -// If the usage count reaches 0, the value will be
27 -// removed from the map. There is no way to overwrite
28 -// existing keys in the pool without first deleting
29 -// it as many times as it was stored. Deleting too
30 -// many times will panic.
24 +// based on usage (reference counting). Values are
25 +// only inserted if they do not already exist. There
26 +// are two ways to add values to the pool:
27 +//
28 +// 1) LoadOrStore will increment usage and store the
29 +// value immediately if it does not already exist
30 +// 2) LoadOrNew will increment usage and construct the
31 +// value immediately if it does not already exist,
32 +// then store that value in the pool. When the
33 +// constructed value is finally deleted from the
34 +// pool (after its usage reaches 0), it will be
35 +// cleaned up by calling its Destruct method.
36 +//
37 +// The use of LoadOrNew allows values to be created
38 +// and reused and finally cleaned up only once, even
39 +// though they may have many references throughout
40 +// their lifespan. This is helpful, for example, when
41 +// sharing thread-safe io.Writers that you only want
42 +// to open and close once.
43 +//
44 +// There is no way to overwrite existing keys in the
45 +// pool without first deleting it as many times as it
46 +// was stored. Deleting too many times will panic.
47 +//
48 +// The implementation does not use a sync.Pool because
49 +// UsagePool needs additional atomicity to run the
50 +// constructor functions when creating a new value when
51 +// LoadOrNew is used. (We could probably use sync.Pool
52 +// but we'd still have to layer our own additional locks
53 +// on top.)
54 //
55 // An empty UsagePool is NOT safe to use; always call
33 -// NewUsagePool() to make a new value.
56 +// NewUsagePool() to make a new one.
57 type UsagePool struct {
35 - pool *sync.Map
58 + sync.RWMutex
59 + pool map[interface{}]*usagePoolVal
60 }
61
38 -// NewUsagePool returns a new usage pool.
62 +// NewUsagePool returns a new usage pool that is ready to use.
63 func NewUsagePool() *UsagePool {
40 - return &UsagePool{pool: new(sync.Map)}
64 + return &UsagePool{
65 + pool: make(map[interface{}]*usagePoolVal),
66 + }
67 +}
68 +
69 +// LoadOrNew loads the value associated with key from the pool if it
70 +// already exists. If the key doesn't exist, it will call construct
71 +// to create a new value and then stores that in the pool. An error
72 +// is only returned if the constructor returns an error. The loaded
73 +// or constructed value is returned. The loaded return value is true
74 +// if the value already existed and was loaded, or false if it was
75 +// newly constructed.
76 +func (up *UsagePool) LoadOrNew(key interface{}, construct Constructor) (value interface{}, loaded bool, err error) {
77 + var upv *usagePoolVal
78 + up.Lock()
79 + upv, loaded = up.pool[key]
80 + if loaded {
81 + atomic.AddInt32(&upv.refs, 1)
82 + up.Unlock()
83 + upv.RLock()
84 + value = upv.value
85 + err = upv.err
86 + upv.RUnlock()
87 + } else {
88 + upv = &usagePoolVal{refs: 1}
89 + upv.Lock()
90 + up.pool[key] = upv
91 + up.Unlock()
92 + value, err = construct()
93 + if err == nil {
94 + upv.value = value
95 + } else {
96 + // TODO: remove error'ed entries from map
97 + upv.err = err
98 + }
99 + upv.Unlock()
100 + }
101 + return
102 +}
103 +
104 +// LoadOrStore loads the value associated with key from the pool if it
105 +// already exists, or stores it if it does not exist. It returns the
106 +// value that was either loaded or stored, and true if the value already
107 +// existed and was
108 +func (up *UsagePool) LoadOrStore(key, val interface{}) (value interface{}, loaded bool) {
109 + var upv *usagePoolVal
110 + up.Lock()
111 + upv, loaded = up.pool[key]
112 + if loaded {
113 + atomic.AddInt32(&upv.refs, 1)
114 + up.Unlock()
115 + upv.Lock()
116 + if upv.err == nil {
117 + value = upv.value
118 + } else {
119 + upv.value = val
120 + upv.err = nil
121 + }
122 + upv.Unlock()
123 + } else {
124 + upv = &usagePoolVal{refs: 1, value: val}
125 + up.pool[key] = upv
126 + up.Unlock()
127 + value = val
128 + }
129 + return
130 +}
131 +
132 +// Range iterates the pool similarly to how sync.Map.Range() does:
133 +// it calls f for every key in the pool, and if f returns false,
134 +// iteration is stopped. Ranging does not affect usage counts.
135 +//
136 +// This method is somewhat naive and acquires a read lock on the
137 +// entire pool during iteration, so do your best to make f() really
138 +// fast, m'kay?
139 +func (up *UsagePool) Range(f func(key, value interface{}) bool) {
140 + up.RLock()
141 + defer up.RUnlock()
142 + for key, upv := range up.pool {
143 + upv.RLock()
144 + if upv.err != nil {
145 + upv.RUnlock()
146 + continue
147 + }
148 + val := upv.value
149 + upv.RUnlock()
150 + if !f(key, val) {
151 + break
152 + }
153 + }
154 }
155
156 // Delete decrements the usage count for key and removes the
@@ -45,50 +158,47 @@ func NewUsagePool() *UsagePool {
158 // true if the usage count reached 0 and the value was deleted.
159 // It panics if the usage count drops below 0; always call
160 // Delete precisely as many times as LoadOrStore.
48 -func (up *UsagePool) Delete(key interface{}) (deleted bool) {
49 - usageVal, ok := up.pool.Load(key)
161 +func (up *UsagePool) Delete(key interface{}) (deleted bool, err error) {
162 + up.Lock()
163 + upv, ok := up.pool[key]
164 if !ok {
51 - return false
165 + up.Unlock()
166 + return false, nil
167 }
53 - upv := usageVal.(*usagePoolVal)
54 - newUsage := atomic.AddInt32(&upv.usage, -1)
55 - if newUsage == 0 {
56 - up.pool.Delete(key)
57 - return true
58 - } else if newUsage < 0 {
59 - panic(fmt.Sprintf("deleted more than stored: %#v (usage: %d)",
60 - upv.value, upv.usage))
61 - }
62 - return false
63 -}
64 -
65 -// LoadOrStore puts val in the pool and returns false if key does
66 -// not already exist; otherwise if the key exists, it loads the
67 -// existing value, increments the usage for that value, and returns
68 -// the value along with true.
69 -func (up *UsagePool) LoadOrStore(key, val interface{}) (actual interface{}, loaded bool) {
70 - usageVal := &usagePoolVal{
71 - usage: 1,
72 - value: val,
73 - }
74 - actual, loaded = up.pool.LoadOrStore(key, usageVal)
75 - if loaded {
76 - upv := actual.(*usagePoolVal)
77 - actual = upv.value
78 - atomic.AddInt32(&upv.usage, 1)
168 + refs := atomic.AddInt32(&upv.refs, -1)
169 + if refs == 0 {
170 + delete(up.pool, key)
171 + up.Unlock()
172 + upv.RLock()
173 + val := upv.value
174 + upv.RUnlock()
175 + if destructor, ok := val.(Destructor); ok {
176 + err = destructor.Destruct()
177 + }
178 + deleted = true
179 + } else {
180 + up.Unlock()
181 + if refs < 0 {
182 + panic(fmt.Sprintf("deleted more than stored: %#v (usage: %d)",
183 + upv.value, upv.refs))
184 + }
185 }
186 return
187 }
188
83 -// Range iterates the pool the same way sync.Map.Range does.
84 -// This does not affect usage counts.
85 -func (up *UsagePool) Range(f func(key, value interface{}) bool) {
86 - up.pool.Range(func(key, value interface{}) bool {
87 - return f(key, value.(*usagePoolVal).value)
88 - })
189 +// Constructor is a function that returns a new value
190 +// that can destruct itself when it is no longer needed.
191 +type Constructor func() (Destructor, error)
192 +
193 +// Destructor is a value that can clean itself up when
194 +// it is deallocated.
195 +type Destructor interface {
196 + Destruct() error
197 }
198
199 type usagePoolVal struct {
92 - usage int32 // accessed atomically; must be 64-bit aligned for 32-bit systems
200 + refs int32 // accessed atomically; must be 64-bit aligned for 32-bit systems
201 value interface{}
202 + err error
203 + sync.RWMutex
204 }