Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 12, 14:30 UTC
caddyhttp: Support multiple logger names per host (#6088)
* caddyhttp: Support multiple logger names per host * Lint * Add adapt test * Implement "string or array" parsing, keep original `logger_names` * Rewrite adapter test to be more representative of the usecase
Francis Lavoie committed
Apr 16, 2024 at 18:26 UTC
70953e873a559553e97c985a13db897ab9f573ff
10 files changed
+249
-62
caddyconfig/httpcaddyfile/httptype.go
+6
-6
@@ -805,22 +805,22 @@ func (st *ServerType) serversFromPairings(
805
// if the logger overrides the hostnames, map that to the logger name
806
for _, h := range ncl.hostnames {
807
if srv.Logs.LoggerNames == nil {
808
- srv.Logs.LoggerNames = make(map[string]string)
808
+ srv.Logs.LoggerNames = make(map[string]caddyhttp.StringArray)
809
}
810
- srv.Logs.LoggerNames[h] = ncl.name
810
+ srv.Logs.LoggerNames[h] = append(srv.Logs.LoggerNames[h], ncl.name)
811
}
812
} else {
813
// otherwise, map each host to the logger name
814
for _, h := range sblockLogHosts {
815
- if srv.Logs.LoggerNames == nil {
816
- srv.Logs.LoggerNames = make(map[string]string)
817
- }
815
// strip the port from the host, if any
816
host, _, err := net.SplitHostPort(h)
817
if err != nil {
818
host = h
819
}
823
- srv.Logs.LoggerNames[host] = ncl.name
820
+ if srv.Logs.LoggerNames == nil {
821
+ srv.Logs.LoggerNames = make(map[string]caddyhttp.StringArray)
822
+ }
823
+ srv.Logs.LoggerNames[host] = append(srv.Logs.LoggerNames[host], ncl.name)
824
}
825
}
826
}
caddytest/integration/caddyfile_adapt/import_args_snippet.caddyfiletest
+6
-2
@@ -72,8 +72,12 @@ b.example.com {
72
],
73
"logs": {
74
"logger_names": {
75
- "a.example.com": "log0",
76
- "b.example.com": "log1"
75
+ "a.example.com": [
76
+ "log0"
77
+ ],
78
+ "b.example.com": [
79
+ "log1"
80
+ ]
81
}
82
}
83
}
caddytest/integration/caddyfile_adapt/log_except_catchall_blocks.caddyfiletest
+3
-1
@@ -99,7 +99,9 @@ http://localhost:2020 {
99
},
100
"logs": {
101
"logger_names": {
102
- "localhost": ""
102
+ "localhost": [
103
+ ""
104
+ ]
105
},
106
"skip_unmapped_hosts": true
107
}
caddytest/integration/caddyfile_adapt/log_multi_logger_name.caddyfiletest
new
+117
@@ -0,0 +1,117 @@
1
+(log-both) {
2
+ log {args[0]}-json {
3
+ hostnames {args[0]}
4
+ output file /var/log/{args[0]}.log
5
+ format json
6
+ }
7
+ log {args[0]}-console {
8
+ hostnames {args[0]}
9
+ output file /var/log/{args[0]}.json
10
+ format console
11
+ }
12
+}
13
+
14
+*.example.com {
15
+ # Subdomains log to multiple files at once, with
16
+ # different output files and formats.
17
+ import log-both foo.example.com
18
+ import log-both bar.example.com
19
+}
20
+----------
21
+{
22
+ "logging": {
23
+ "logs": {
24
+ "bar.example.com-console": {
25
+ "writer": {
26
+ "filename": "/var/log/bar.example.com.json",
27
+ "output": "file"
28
+ },
29
+ "encoder": {
30
+ "format": "console"
31
+ },
32
+ "include": [
33
+ "http.log.access.bar.example.com-console"
34
+ ]
35
+ },
36
+ "bar.example.com-json": {
37
+ "writer": {
38
+ "filename": "/var/log/bar.example.com.log",
39
+ "output": "file"
40
+ },
41
+ "encoder": {
42
+ "format": "json"
43
+ },
44
+ "include": [
45
+ "http.log.access.bar.example.com-json"
46
+ ]
47
+ },
48
+ "default": {
49
+ "exclude": [
50
+ "http.log.access.bar.example.com-console",
51
+ "http.log.access.bar.example.com-json",
52
+ "http.log.access.foo.example.com-console",
53
+ "http.log.access.foo.example.com-json"
54
+ ]
55
+ },
56
+ "foo.example.com-console": {
57
+ "writer": {
58
+ "filename": "/var/log/foo.example.com.json",
59
+ "output": "file"
60
+ },
61
+ "encoder": {
62
+ "format": "console"
63
+ },
64
+ "include": [
65
+ "http.log.access.foo.example.com-console"
66
+ ]
67
+ },
68
+ "foo.example.com-json": {
69
+ "writer": {
70
+ "filename": "/var/log/foo.example.com.log",
71
+ "output": "file"
72
+ },
73
+ "encoder": {
74
+ "format": "json"
75
+ },
76
+ "include": [
77
+ "http.log.access.foo.example.com-json"
78
+ ]
79
+ }
80
+ }
81
+ },
82
+ "apps": {
83
+ "http": {
84
+ "servers": {
85
+ "srv0": {
86
+ "listen": [
87
+ ":443"
88
+ ],
89
+ "routes": [
90
+ {
91
+ "match": [
92
+ {
93
+ "host": [
94
+ "*.example.com"
95
+ ]
96
+ }
97
+ ],
98
+ "terminal": true
99
+ }
100
+ ],
101
+ "logs": {
102
+ "logger_names": {
103
+ "bar.example.com": [
104
+ "bar.example.com-json",
105
+ "bar.example.com-console"
106
+ ],
107
+ "foo.example.com": [
108
+ "foo.example.com-json",
109
+ "foo.example.com-console"
110
+ ]
111
+ }
112
+ }
113
+ }
114
+ }
115
+ }
116
+ }
117
+}
\ No newline at end of file
caddytest/integration/caddyfile_adapt/log_override_hostname.caddyfiletest
+12
-4
@@ -75,9 +75,15 @@ example.com:8443 {
75
],
76
"logs": {
77
"logger_names": {
78
- "bar.example.com": "log0",
79
- "baz.example.com": "log1",
80
- "foo.example.com": "log0"
78
+ "bar.example.com": [
79
+ "log0"
80
+ ],
81
+ "baz.example.com": [
82
+ "log1"
83
+ ],
84
+ "foo.example.com": [
85
+ "log0"
86
+ ]
87
}
88
}
89
},
@@ -99,7 +105,9 @@ example.com:8443 {
105
],
106
"logs": {
107
"logger_names": {
102
- "example.com": "log2"
108
+ "example.com": [
109
+ "log2"
110
+ ]
111
}
112
}
113
}
caddytest/integration/caddyfile_adapt/log_override_name_multiaccess.caddyfiletest
+3
-1
@@ -76,7 +76,9 @@ http://localhost:8881 {
76
},
77
"logs": {
78
"logger_names": {
79
- "localhost": "foo"
79
+ "localhost": [
80
+ "foo"
81
+ ]
82
}
83
}
84
}
caddytest/integration/caddyfile_adapt/log_override_name_multiaccess_debug.caddyfiletest
+3
-1
@@ -81,7 +81,9 @@ http://localhost:8881 {
81
},
82
"logs": {
83
"logger_names": {
84
- "localhost": "foo"
84
+ "localhost": [
85
+ "foo"
86
+ ]
87
}
88
}
89
}
caddytest/integration/caddyfile_adapt/log_skip_hosts.caddyfiletest
+3
-1
@@ -63,7 +63,9 @@ example.com {
63
],
64
"logs": {
65
"logger_names": {
66
- "one.example.com": ""
66
+ "one.example.com": [
67
+ ""
68
+ ]
69
},
70
"skip_hosts": [
71
"example.com",
modules/caddyhttp/logging.go
+67
-26
@@ -15,6 +15,7 @@
15
package caddyhttp
16
17
import (
18
+ "encoding/json"
19
"errors"
20
"net"
21
"net/http"
@@ -32,14 +33,17 @@ import (
33
// customized per-request-host.
34
type ServerLogConfig struct {
35
// The default logger name for all logs emitted by this server for
35
- // hostnames that are not in the LoggerNames (logger_names) map.
36
+ // hostnames that are not in the logger_names map.
37
DefaultLoggerName string `json:"default_logger_name,omitempty"`
38
38
- // LoggerNames maps request hostnames to a custom logger name.
39
- // For example, a mapping of "example.com" to "example" would
40
- // cause access logs from requests with a Host of example.com
41
- // to be emitted by a logger named "http.log.access.example".
42
- LoggerNames map[string]string `json:"logger_names,omitempty"`
39
+ // LoggerNames maps request hostnames to one or more custom logger
40
+ // names. For example, a mapping of "example.com" to "example" would
41
+ // cause access logs from requests with a Host of example.com to be
42
+ // emitted by a logger named "http.log.access.example". If there are
43
+ // multiple logger names, then the log will be emitted to all of them.
44
+ // For backwards compatibility, if the value is a string, it is treated
45
+ // as a single-element array.
46
+ LoggerNames map[string]StringArray `json:"logger_names,omitempty"`
47
48
// By default, all requests to this server will be logged if
49
// access logging is enabled. This field lists the request
@@ -47,7 +51,7 @@ type ServerLogConfig struct {
51
SkipHosts []string `json:"skip_hosts,omitempty"`
52
53
// If true, requests to any host not appearing in the
50
- // LoggerNames (logger_names) map will not be logged.
54
+ // logger_names map will not be logged.
55
SkipUnmappedHosts bool `json:"skip_unmapped_hosts,omitempty"`
56
57
// If true, credentials that are otherwise omitted, will be logged.
@@ -57,33 +61,39 @@ type ServerLogConfig struct {
61
ShouldLogCredentials bool `json:"should_log_credentials,omitempty"`
62
}
63
60
-// wrapLogger wraps logger in a logger named according to user preferences for the given host.
61
-func (slc ServerLogConfig) wrapLogger(logger *zap.Logger, host string) *zap.Logger {
62
- if loggerName := slc.getLoggerName(host); loggerName != "" {
63
- return logger.Named(loggerName)
64
+// wrapLogger wraps logger in one or more logger named
65
+// according to user preferences for the given host.
66
+func (slc ServerLogConfig) wrapLogger(logger *zap.Logger, host string) []*zap.Logger {
67
+ hosts := slc.getLoggerHosts(host)
68
+ loggers := make([]*zap.Logger, 0, len(hosts))
69
+ for _, loggerName := range hosts {
70
+ if loggerName == "" {
71
+ continue
72
+ }
73
+ loggers = append(loggers, logger.Named(loggerName))
74
}
65
- return logger
75
+ return loggers
76
}
77
68
-func (slc ServerLogConfig) getLoggerName(host string) string {
69
- tryHost := func(key string) (string, bool) {
78
+func (slc ServerLogConfig) getLoggerHosts(host string) []string {
79
+ tryHost := func(key string) ([]string, bool) {
80
// first try exact match
71
- if loggerName, ok := slc.LoggerNames[key]; ok {
72
- return loggerName, ok
81
+ if hosts, ok := slc.LoggerNames[key]; ok {
82
+ return hosts, ok
83
}
84
// strip port and try again (i.e. Host header of "example.com:1234" should
85
// match "example.com" if there is no "example.com:1234" in the map)
86
hostOnly, _, err := net.SplitHostPort(key)
87
if err != nil {
78
- return "", false
88
+ return []string{}, false
89
}
80
- loggerName, ok := slc.LoggerNames[hostOnly]
81
- return loggerName, ok
90
+ hosts, ok := slc.LoggerNames[hostOnly]
91
+ return hosts, ok
92
}
93
94
// try the exact hostname first
85
- if loggerName, ok := tryHost(host); ok {
86
- return loggerName
95
+ if hosts, ok := tryHost(host); ok {
96
+ return hosts
97
}
98
99
// try matching wildcard domains if other non-specific loggers exist
@@ -94,28 +104,59 @@ func (slc ServerLogConfig) getLoggerName(host string) string {
104
}
105
labels[i] = "*"
106
wildcardHost := strings.Join(labels, ".")
97
- if loggerName, ok := tryHost(wildcardHost); ok {
98
- return loggerName
107
+ if hosts, ok := tryHost(wildcardHost); ok {
108
+ return hosts
109
}
110
}
111
102
- return slc.DefaultLoggerName
112
+ return []string{slc.DefaultLoggerName}
113
}
114
115
func (slc *ServerLogConfig) clone() *ServerLogConfig {
116
clone := &ServerLogConfig{
117
DefaultLoggerName: slc.DefaultLoggerName,
108
- LoggerNames: make(map[string]string),
118
+ LoggerNames: make(map[string]StringArray),
119
SkipHosts: append([]string{}, slc.SkipHosts...),
120
SkipUnmappedHosts: slc.SkipUnmappedHosts,
121
ShouldLogCredentials: slc.ShouldLogCredentials,
122
}
123
for k, v := range slc.LoggerNames {
114
- clone.LoggerNames[k] = v
124
+ clone.LoggerNames[k] = append([]string{}, v...)
125
}
126
return clone
127
}
128
129
+// StringArray is a slices of strings, but also accepts
130
+// a single string as a value when JSON unmarshaling,
131
+// converting it to a slice of one string.
132
+type StringArray []string
133
+
134
+// UnmarshalJSON satisfies json.Unmarshaler.
135
+func (sa *StringArray) UnmarshalJSON(b []byte) error {
136
+ var jsonObj any
137
+ err := json.Unmarshal(b, &jsonObj)
138
+ if err != nil {
139
+ return err
140
+ }
141
+ switch obj := jsonObj.(type) {
142
+ case string:
143
+ *sa = StringArray([]string{obj})
144
+ return nil
145
+ case []any:
146
+ s := make([]string, 0, len(obj))
147
+ for _, v := range obj {
148
+ value, ok := v.(string)
149
+ if !ok {
150
+ return errors.New("unsupported type")
151
+ }
152
+ s = append(s, value)
153
+ }
154
+ *sa = StringArray(s)
155
+ return nil
156
+ }
157
+ return errors.New("unsupported type")
158
+}
159
+
160
// errLogValues inspects err and returns the status code
161
// to use, the error log message, and any extra fields.
162
// If err is a HandlerError, the returned values will
modules/caddyhttp/server.go
+29
-20
@@ -361,11 +361,11 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
361
cloneURL(origReq.URL, r.URL)
362
363
// prepare the error log
364
- logger := errLog
364
+ errLog = errLog.With(zap.Duration("duration", duration))
365
+ errLoggers := []*zap.Logger{errLog}
366
if s.Logs != nil {
366
- logger = s.Logs.wrapLogger(logger, r.Host)
367
+ errLoggers = s.Logs.wrapLogger(errLog, r.Host)
368
}
368
- logger = logger.With(zap.Duration("duration", duration))
369
370
// get the values that will be used to log the error
371
errStatus, errMsg, errFields := errLogValues(err)
@@ -379,7 +379,9 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
379
if err2 == nil {
380
// user's error route handled the error response
381
// successfully, so now just log the error
382
- logger.Debug(errMsg, errFields...)
382
+ for _, logger := range errLoggers {
383
+ logger.Debug(errMsg, errFields...)
384
+ }
385
} else {
386
// well... this is awkward
387
errFields = append([]zapcore.Field{
@@ -387,7 +389,9 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
389
zap.Namespace("first_error"),
390
zap.String("msg", errMsg),
391
}, errFields...)
390
- logger.Error("error handling handler error", errFields...)
392
+ for _, logger := range errLoggers {
393
+ logger.Error("error handling handler error", errFields...)
394
+ }
395
if handlerErr, ok := err.(HandlerError); ok {
396
w.WriteHeader(handlerErr.StatusCode)
397
} else {
@@ -395,10 +399,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
399
}
400
}
401
} else {
398
- if errStatus >= 500 {
399
- logger.Error(errMsg, errFields...)
400
- } else {
401
- logger.Debug(errMsg, errFields...)
402
+ for _, logger := range errLoggers {
403
+ if errStatus >= 500 {
404
+ logger.Error(errMsg, errFields...)
405
+ } else {
406
+ logger.Debug(errMsg, errFields...)
407
+ }
408
}
409
w.WriteHeader(errStatus)
410
}
@@ -735,16 +741,6 @@ func (s *Server) logRequest(
741
repl.Set("http.response.duration", duration)
742
repl.Set("http.response.duration_ms", duration.Seconds()*1e3) // multiply seconds to preserve decimal (see #4666)
743
738
- logger := accLog
739
- if s.Logs != nil {
740
- logger = s.Logs.wrapLogger(logger, r.Host)
741
- }
742
-
743
- log := logger.Info
744
- if wrec.Status() >= 400 {
745
- log = logger.Error
746
- }
747
-
744
userID, _ := repl.GetString("http.auth.user.id")
745
746
reqBodyLength := 0
@@ -768,7 +764,20 @@ func (s *Server) logRequest(
764
}))
765
fields = append(fields, extra.fields...)
766
771
- log("handled request", fields...)
767
+ loggers := []*zap.Logger{accLog}
768
+ if s.Logs != nil {
769
+ loggers = s.Logs.wrapLogger(accLog, r.Host)
770
+ }
771
+
772
+ // wrapping may return multiple loggers, so we log to all of them
773
+ for _, logger := range loggers {
774
+ logAtLevel := logger.Info
775
+ if wrec.Status() >= 400 {
776
+ logAtLevel = logger.Error
777
+ }
778
+
779
+ logAtLevel("handled request", fields...)
780
+ }
781
}
782
783
// protocol returns true if the protocol proto is configured/enabled.