@marcel91200 / caddy Mirror · read-only / commits / 70953e87
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 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.