From 4356635d123ee71b72654738a0c30653a3a29d98 Mon Sep 17 00:00:00 2001 From: Aziz Rmadi <46684200+armadi1809@users.noreply.github.com> Date: Sat, 11 May 2024 08:31:44 -0500 Subject: [PATCH] logging: Add support for additional logger filters other than hostname (#6082) Co-authored-by: Francis Lavoie --- caddyconfig/httpcaddyfile/builtins.go | 21 ++- caddyconfig/httpcaddyfile/directives.go | 1 + caddyconfig/httpcaddyfile/httptype.go | 16 +- .../log_filter_with_header.txt | 151 ++++++++++++++++++ modules/caddyhttp/logging.go | 38 ++++- modules/caddyhttp/server.go | 5 +- 6 files changed, 217 insertions(+), 15 deletions(-) create mode 100644 caddytest/integration/caddyfile_adapt/log_filter_with_header.txt diff --git a/caddyconfig/httpcaddyfile/builtins.go b/caddyconfig/httpcaddyfile/builtins.go index 27ab0a18..35a08ef2 100644 --- a/caddyconfig/httpcaddyfile/builtins.go +++ b/caddyconfig/httpcaddyfile/builtins.go @@ -51,6 +51,7 @@ func init() { RegisterDirective("log", parseLog) RegisterHandlerDirective("skip_log", parseLogSkip) RegisterHandlerDirective("log_skip", parseLogSkip) + RegisterHandlerDirective("log_name", parseLogName) } // parseBind parses the bind directive. Syntax: @@ -914,7 +915,7 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue // this is useful for setting up loggers per subdomain in a site block // with a wildcard domain customHostnames := []string{} - + noHostname := false for h.NextBlock(0) { switch h.Val() { case "hostnames": @@ -1000,6 +1001,12 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue cl.Exclude = append(cl.Exclude, h.Val()) } + case "no_hostname": + if h.NextArg() { + return nil, h.ArgErr() + } + noHostname = true + default: return nil, h.Errf("unrecognized subdirective: %s", h.Val()) } @@ -1007,7 +1014,7 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue var val namedCustomLog val.hostnames = customHostnames - + val.noHostname = noHostname isEmptyConfig := reflect.DeepEqual(cl, new(caddy.CustomLog)) // Skip handling of empty logging configs @@ -1058,3 +1065,13 @@ func parseLogSkip(h Helper) (caddyhttp.MiddlewareHandler, error) { } return caddyhttp.VarsMiddleware{"log_skip": true}, nil } + +// parseLogName parses the log_name directive. Syntax: +// +// log_name +func parseLogName(h Helper) (caddyhttp.MiddlewareHandler, error) { + h.Next() // consume directive name + return caddyhttp.VarsMiddleware{ + caddyhttp.AccessLoggerNameVarKey: h.RemainingArgs(), + }, nil +} diff --git a/caddyconfig/httpcaddyfile/directives.go b/caddyconfig/httpcaddyfile/directives.go index 17259421..3e688ebc 100644 --- a/caddyconfig/httpcaddyfile/directives.go +++ b/caddyconfig/httpcaddyfile/directives.go @@ -53,6 +53,7 @@ var defaultDirectiveOrder = []string{ "log_append", "skip_log", // TODO: deprecated, renamed to log_skip "log_skip", + "log_name", "header", "copy_response_headers", // only in reverse_proxy's handle_response diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go index 8e7d21fa..a8a2ae5b 100644 --- a/caddyconfig/httpcaddyfile/httptype.go +++ b/caddyconfig/httpcaddyfile/httptype.go @@ -797,6 +797,15 @@ func (st *ServerType) serversFromPairings( sblockLogHosts := sblock.hostsFromKeys(true) for _, cval := range sblock.pile["custom_log"] { ncl := cval.Value.(namedCustomLog) + + // if `no_hostname` is set, then this logger will not + // be associated with any of the site block's hostnames, + // and only be usable via the `log_name` directive + // or the `access_logger_names` variable + if ncl.noHostname { + continue + } + if sblock.hasHostCatchAllKey() && len(ncl.hostnames) == 0 { // all requests for hosts not able to be listed should use // this log because it's a catch-all-hosts server block @@ -1598,9 +1607,10 @@ func (c counter) nextGroup() string { } type namedCustomLog struct { - name string - hostnames []string - log *caddy.CustomLog + name string + hostnames []string + log *caddy.CustomLog + noHostname bool } // sbAddrAssociation is a mapping from a list of diff --git a/caddytest/integration/caddyfile_adapt/log_filter_with_header.txt b/caddytest/integration/caddyfile_adapt/log_filter_with_header.txt new file mode 100644 index 00000000..3ab6d624 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/log_filter_with_header.txt @@ -0,0 +1,151 @@ +localhost { + log { + output file ./caddy.access.log + } + log health_check_log { + output file ./caddy.access.health.log + no_hostname + } + log general_log { + output file ./caddy.access.general.log + no_hostname + } + @healthCheck `header_regexp('User-Agent', '^some-regexp$') || path('/healthz*')` + handle @healthCheck { + log_name health_check_log general_log + respond "Healthy" + } + + handle { + respond "Hello World" + } +} +---------- +{ + "logging": { + "logs": { + "default": { + "exclude": [ + "http.log.access.general_log", + "http.log.access.health_check_log", + "http.log.access.log0" + ] + }, + "general_log": { + "writer": { + "filename": "./caddy.access.general.log", + "output": "file" + }, + "include": [ + "http.log.access.general_log" + ] + }, + "health_check_log": { + "writer": { + "filename": "./caddy.access.health.log", + "output": "file" + }, + "include": [ + "http.log.access.health_check_log" + ] + }, + "log0": { + "writer": { + "filename": "./caddy.access.log", + "output": "file" + }, + "include": [ + "http.log.access.log0" + ] + } + } + }, + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "localhost" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "group": "group2", + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "access_logger_names": [ + "health_check_log", + "general_log" + ], + "handler": "vars" + }, + { + "body": "Healthy", + "handler": "static_response" + } + ] + } + ] + } + ], + "match": [ + { + "expression": { + "expr": "header_regexp('User-Agent', '^some-regexp$') || path('/healthz*')", + "name": "healthCheck" + } + } + ] + }, + { + "group": "group2", + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "Hello World", + "handler": "static_response" + } + ] + } + ] + } + ] + } + ] + } + ], + "terminal": true + } + ], + "logs": { + "logger_names": { + "localhost": [ + "log0" + ] + } + } + } + } + } + } +} diff --git a/modules/caddyhttp/logging.go b/modules/caddyhttp/logging.go index 774bd22f..1fab1e8e 100644 --- a/modules/caddyhttp/logging.go +++ b/modules/caddyhttp/logging.go @@ -69,15 +69,35 @@ type ServerLogConfig struct { // 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 { - // logger config should always be only - // the hostname, without the port - hostWithoutPort, _, err := net.SplitHostPort(host) - if err != nil { - hostWithoutPort = host +func (slc ServerLogConfig) wrapLogger(logger *zap.Logger, req *http.Request) []*zap.Logger { + // using the `log_name` directive or the `access_logger_names` variable, + // the logger names can be overridden for the current request + if names := GetVar(req.Context(), AccessLoggerNameVarKey); names != nil { + if namesSlice, ok := names.([]any); ok { + loggers := make([]*zap.Logger, 0, len(namesSlice)) + for _, loggerName := range namesSlice { + // no name, use the default logger + if loggerName == "" { + loggers = append(loggers, logger) + continue + } + // make a logger with the given name + loggers = append(loggers, logger.Named(loggerName.(string))) + } + return loggers + } } - hosts := slc.getLoggerHosts(hostWithoutPort) + // get the hostname from the request, with the port number stripped + host, _, err := net.SplitHostPort(req.Host) + if err != nil { + host = req.Host + } + + // get the logger names for this host from the config + hosts := slc.getLoggerHosts(host) + + // make a list of named loggers, or the default logger loggers := make([]*zap.Logger, 0, len(hosts)) for _, loggerName := range hosts { // no name, use the default logger @@ -85,6 +105,7 @@ func (slc ServerLogConfig) wrapLogger(logger *zap.Logger, host string) []*zap.Lo loggers = append(loggers, logger) continue } + // make a logger with the given name loggers = append(loggers, logger.Named(loggerName)) } return loggers @@ -211,4 +232,7 @@ const ( // For adding additional fields to the access logs ExtraLogFieldsCtxKey caddy.CtxKey = "extra_log_fields" + + // Variable name used to indicate the logger to be used + AccessLoggerNameVarKey string = "access_logger_names" ) diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index 408f3358..1d621456 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -369,7 +369,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { errLog = errLog.With(zap.Duration("duration", duration)) errLoggers := []*zap.Logger{errLog} if s.Logs != nil { - errLoggers = s.Logs.wrapLogger(errLog, r.Host) + errLoggers = s.Logs.wrapLogger(errLog, r) } // get the values that will be used to log the error @@ -778,7 +778,7 @@ func (s *Server) logRequest( loggers := []*zap.Logger{accLog} if s.Logs != nil { - loggers = s.Logs.wrapLogger(accLog, r.Host) + loggers = s.Logs.wrapLogger(accLog, r) } // wrapping may return multiple loggers, so we log to all of them @@ -835,7 +835,6 @@ func PrepareRequest(r *http.Request, repl *caddy.Replacer, w http.ResponseWriter ctx = context.WithValue(ctx, OriginalRequestCtxKey, originalRequest(r, &url2)) ctx = context.WithValue(ctx, ExtraLogFieldsCtxKey, new(ExtraLogFields)) - r = r.WithContext(ctx) // once the pointer to the request won't change