diff options
author | Francis Lavoie <[email protected]> | 2024-04-16 18:26:18 -0400 |
---|---|---|
committer | GitHub <[email protected]> | 2024-04-16 22:26:18 +0000 |
commit | 70953e873a559553e97c985a13db897ab9f573ff (patch) | |
tree | cade4037d41f21a0fb16ca879f098d6d010ea010 /modules/caddyhttp | |
parent | eafc875ea9f6238fce24b23af47a1d7162fd27a5 (diff) | |
download | caddy-70953e873a559553e97c985a13db897ab9f573ff.tar.gz caddy-70953e873a559553e97c985a13db897ab9f573ff.zip |
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
Diffstat (limited to 'modules/caddyhttp')
-rw-r--r-- | modules/caddyhttp/logging.go | 93 | ||||
-rw-r--r-- | modules/caddyhttp/server.go | 49 |
2 files changed, 96 insertions, 46 deletions
diff --git a/modules/caddyhttp/logging.go b/modules/caddyhttp/logging.go index 81b2830fc..313f12e12 100644 --- a/modules/caddyhttp/logging.go +++ b/modules/caddyhttp/logging.go @@ -15,6 +15,7 @@ package caddyhttp import ( + "encoding/json" "errors" "net" "net/http" @@ -32,14 +33,17 @@ import ( // customized per-request-host. type ServerLogConfig struct { // The default logger name for all logs emitted by this server for - // hostnames that are not in the LoggerNames (logger_names) map. + // hostnames that are not in the logger_names map. DefaultLoggerName string `json:"default_logger_name,omitempty"` - // LoggerNames maps request hostnames to a custom logger name. - // For example, a mapping of "example.com" to "example" would - // cause access logs from requests with a Host of example.com - // to be emitted by a logger named "http.log.access.example". - LoggerNames map[string]string `json:"logger_names,omitempty"` + // LoggerNames maps request hostnames to one or more custom logger + // names. For example, a mapping of "example.com" to "example" would + // cause access logs from requests with a Host of example.com to be + // emitted by a logger named "http.log.access.example". If there are + // multiple logger names, then the log will be emitted to all of them. + // For backwards compatibility, if the value is a string, it is treated + // as a single-element array. + LoggerNames map[string]StringArray `json:"logger_names,omitempty"` // By default, all requests to this server will be logged if // access logging is enabled. This field lists the request @@ -47,7 +51,7 @@ type ServerLogConfig struct { SkipHosts []string `json:"skip_hosts,omitempty"` // If true, requests to any host not appearing in the - // LoggerNames (logger_names) map will not be logged. + // logger_names map will not be logged. SkipUnmappedHosts bool `json:"skip_unmapped_hosts,omitempty"` // If true, credentials that are otherwise omitted, will be logged. @@ -57,33 +61,39 @@ type ServerLogConfig struct { ShouldLogCredentials bool `json:"should_log_credentials,omitempty"` } -// wrapLogger wraps logger in a logger named according to user preferences for the given host. -func (slc ServerLogConfig) wrapLogger(logger *zap.Logger, host string) *zap.Logger { - if loggerName := slc.getLoggerName(host); loggerName != "" { - return logger.Named(loggerName) +// wrapLogger wraps logger in one or more logger named +// according to user preferences for the given host. +func (slc ServerLogConfig) wrapLogger(logger *zap.Logger, host string) []*zap.Logger { + hosts := slc.getLoggerHosts(host) + loggers := make([]*zap.Logger, 0, len(hosts)) + for _, loggerName := range hosts { + if loggerName == "" { + continue + } + loggers = append(loggers, logger.Named(loggerName)) } - return logger + return loggers } -func (slc ServerLogConfig) getLoggerName(host string) string { - tryHost := func(key string) (string, bool) { +func (slc ServerLogConfig) getLoggerHosts(host string) []string { + tryHost := func(key string) ([]string, bool) { // first try exact match - if loggerName, ok := slc.LoggerNames[key]; ok { - return loggerName, ok + if hosts, ok := slc.LoggerNames[key]; ok { + return hosts, ok } // strip port and try again (i.e. Host header of "example.com:1234" should // match "example.com" if there is no "example.com:1234" in the map) hostOnly, _, err := net.SplitHostPort(key) if err != nil { - return "", false + return []string{}, false } - loggerName, ok := slc.LoggerNames[hostOnly] - return loggerName, ok + hosts, ok := slc.LoggerNames[hostOnly] + return hosts, ok } // try the exact hostname first - if loggerName, ok := tryHost(host); ok { - return loggerName + if hosts, ok := tryHost(host); ok { + return hosts } // try matching wildcard domains if other non-specific loggers exist @@ -94,28 +104,59 @@ func (slc ServerLogConfig) getLoggerName(host string) string { } labels[i] = "*" wildcardHost := strings.Join(labels, ".") - if loggerName, ok := tryHost(wildcardHost); ok { - return loggerName + if hosts, ok := tryHost(wildcardHost); ok { + return hosts } } - return slc.DefaultLoggerName + return []string{slc.DefaultLoggerName} } func (slc *ServerLogConfig) clone() *ServerLogConfig { clone := &ServerLogConfig{ DefaultLoggerName: slc.DefaultLoggerName, - LoggerNames: make(map[string]string), + LoggerNames: make(map[string]StringArray), SkipHosts: append([]string{}, slc.SkipHosts...), SkipUnmappedHosts: slc.SkipUnmappedHosts, ShouldLogCredentials: slc.ShouldLogCredentials, } for k, v := range slc.LoggerNames { - clone.LoggerNames[k] = v + clone.LoggerNames[k] = append([]string{}, v...) } return clone } +// StringArray is a slices of strings, but also accepts +// a single string as a value when JSON unmarshaling, +// converting it to a slice of one string. +type StringArray []string + +// UnmarshalJSON satisfies json.Unmarshaler. +func (sa *StringArray) UnmarshalJSON(b []byte) error { + var jsonObj any + err := json.Unmarshal(b, &jsonObj) + if err != nil { + return err + } + switch obj := jsonObj.(type) { + case string: + *sa = StringArray([]string{obj}) + return nil + case []any: + s := make([]string, 0, len(obj)) + for _, v := range obj { + value, ok := v.(string) + if !ok { + return errors.New("unsupported type") + } + s = append(s, value) + } + *sa = StringArray(s) + return nil + } + return errors.New("unsupported type") +} + // errLogValues inspects err and returns the status code // to use, the error log message, and any extra fields. // If err is a HandlerError, the returned values will diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index c7e5a5f61..0e88ef26d 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -361,11 +361,11 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { cloneURL(origReq.URL, r.URL) // prepare the error log - logger := errLog + errLog = errLog.With(zap.Duration("duration", duration)) + errLoggers := []*zap.Logger{errLog} if s.Logs != nil { - logger = s.Logs.wrapLogger(logger, r.Host) + errLoggers = s.Logs.wrapLogger(errLog, r.Host) } - logger = logger.With(zap.Duration("duration", duration)) // get the values that will be used to log the error errStatus, errMsg, errFields := errLogValues(err) @@ -379,7 +379,9 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { if err2 == nil { // user's error route handled the error response // successfully, so now just log the error - logger.Debug(errMsg, errFields...) + for _, logger := range errLoggers { + logger.Debug(errMsg, errFields...) + } } else { // well... this is awkward errFields = append([]zapcore.Field{ @@ -387,7 +389,9 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { zap.Namespace("first_error"), zap.String("msg", errMsg), }, errFields...) - logger.Error("error handling handler error", errFields...) + for _, logger := range errLoggers { + logger.Error("error handling handler error", errFields...) + } if handlerErr, ok := err.(HandlerError); ok { w.WriteHeader(handlerErr.StatusCode) } else { @@ -395,10 +399,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } } else { - if errStatus >= 500 { - logger.Error(errMsg, errFields...) - } else { - logger.Debug(errMsg, errFields...) + for _, logger := range errLoggers { + if errStatus >= 500 { + logger.Error(errMsg, errFields...) + } else { + logger.Debug(errMsg, errFields...) + } } w.WriteHeader(errStatus) } @@ -735,16 +741,6 @@ func (s *Server) logRequest( repl.Set("http.response.duration", duration) repl.Set("http.response.duration_ms", duration.Seconds()*1e3) // multiply seconds to preserve decimal (see #4666) - logger := accLog - if s.Logs != nil { - logger = s.Logs.wrapLogger(logger, r.Host) - } - - log := logger.Info - if wrec.Status() >= 400 { - log = logger.Error - } - userID, _ := repl.GetString("http.auth.user.id") reqBodyLength := 0 @@ -768,7 +764,20 @@ func (s *Server) logRequest( })) fields = append(fields, extra.fields...) - log("handled request", fields...) + loggers := []*zap.Logger{accLog} + if s.Logs != nil { + loggers = s.Logs.wrapLogger(accLog, r.Host) + } + + // wrapping may return multiple loggers, so we log to all of them + for _, logger := range loggers { + logAtLevel := logger.Info + if wrec.Status() >= 400 { + logAtLevel = logger.Error + } + + logAtLevel("handled request", fields...) + } } // protocol returns true if the protocol proto is configured/enabled. |