logging: Add support for additional logger filters other than hostname (#6082)

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
This commit is contained in:
Aziz Rmadi 2024-05-11 08:31:44 -05:00 committed by GitHub
parent 4af38e5ac8
commit 4356635d12
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 217 additions and 15 deletions

View file

@ -51,6 +51,7 @@ func init() {
RegisterDirective("log", parseLog) RegisterDirective("log", parseLog)
RegisterHandlerDirective("skip_log", parseLogSkip) RegisterHandlerDirective("skip_log", parseLogSkip)
RegisterHandlerDirective("log_skip", parseLogSkip) RegisterHandlerDirective("log_skip", parseLogSkip)
RegisterHandlerDirective("log_name", parseLogName)
} }
// parseBind parses the bind directive. Syntax: // 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 // this is useful for setting up loggers per subdomain in a site block
// with a wildcard domain // with a wildcard domain
customHostnames := []string{} customHostnames := []string{}
noHostname := false
for h.NextBlock(0) { for h.NextBlock(0) {
switch h.Val() { switch h.Val() {
case "hostnames": case "hostnames":
@ -1000,6 +1001,12 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
cl.Exclude = append(cl.Exclude, h.Val()) cl.Exclude = append(cl.Exclude, h.Val())
} }
case "no_hostname":
if h.NextArg() {
return nil, h.ArgErr()
}
noHostname = true
default: default:
return nil, h.Errf("unrecognized subdirective: %s", h.Val()) 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 var val namedCustomLog
val.hostnames = customHostnames val.hostnames = customHostnames
val.noHostname = noHostname
isEmptyConfig := reflect.DeepEqual(cl, new(caddy.CustomLog)) isEmptyConfig := reflect.DeepEqual(cl, new(caddy.CustomLog))
// Skip handling of empty logging configs // Skip handling of empty logging configs
@ -1058,3 +1065,13 @@ func parseLogSkip(h Helper) (caddyhttp.MiddlewareHandler, error) {
} }
return caddyhttp.VarsMiddleware{"log_skip": true}, nil return caddyhttp.VarsMiddleware{"log_skip": true}, nil
} }
// parseLogName parses the log_name directive. Syntax:
//
// log_name <names...>
func parseLogName(h Helper) (caddyhttp.MiddlewareHandler, error) {
h.Next() // consume directive name
return caddyhttp.VarsMiddleware{
caddyhttp.AccessLoggerNameVarKey: h.RemainingArgs(),
}, nil
}

View file

@ -53,6 +53,7 @@ var defaultDirectiveOrder = []string{
"log_append", "log_append",
"skip_log", // TODO: deprecated, renamed to log_skip "skip_log", // TODO: deprecated, renamed to log_skip
"log_skip", "log_skip",
"log_name",
"header", "header",
"copy_response_headers", // only in reverse_proxy's handle_response "copy_response_headers", // only in reverse_proxy's handle_response

View file

@ -797,6 +797,15 @@ func (st *ServerType) serversFromPairings(
sblockLogHosts := sblock.hostsFromKeys(true) sblockLogHosts := sblock.hostsFromKeys(true)
for _, cval := range sblock.pile["custom_log"] { for _, cval := range sblock.pile["custom_log"] {
ncl := cval.Value.(namedCustomLog) 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 { if sblock.hasHostCatchAllKey() && len(ncl.hostnames) == 0 {
// all requests for hosts not able to be listed should use // all requests for hosts not able to be listed should use
// this log because it's a catch-all-hosts server block // this log because it's a catch-all-hosts server block
@ -1601,6 +1610,7 @@ type namedCustomLog struct {
name string name string
hostnames []string hostnames []string
log *caddy.CustomLog log *caddy.CustomLog
noHostname bool
} }
// sbAddrAssociation is a mapping from a list of // sbAddrAssociation is a mapping from a list of

View file

@ -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"
]
}
}
}
}
}
}
}

View file

@ -69,15 +69,35 @@ type ServerLogConfig struct {
// wrapLogger wraps logger in one or more logger named // wrapLogger wraps logger in one or more logger named
// according to user preferences for the given host. // according to user preferences for the given host.
func (slc ServerLogConfig) wrapLogger(logger *zap.Logger, host string) []*zap.Logger { func (slc ServerLogConfig) wrapLogger(logger *zap.Logger, req *http.Request) []*zap.Logger {
// logger config should always be only // using the `log_name` directive or the `access_logger_names` variable,
// the hostname, without the port // the logger names can be overridden for the current request
hostWithoutPort, _, err := net.SplitHostPort(host) if names := GetVar(req.Context(), AccessLoggerNameVarKey); names != nil {
if err != nil { if namesSlice, ok := names.([]any); ok {
hostWithoutPort = host 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)) loggers := make([]*zap.Logger, 0, len(hosts))
for _, loggerName := range hosts { for _, loggerName := range hosts {
// no name, use the default logger // 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) loggers = append(loggers, logger)
continue continue
} }
// make a logger with the given name
loggers = append(loggers, logger.Named(loggerName)) loggers = append(loggers, logger.Named(loggerName))
} }
return loggers return loggers
@ -211,4 +232,7 @@ const (
// For adding additional fields to the access logs // For adding additional fields to the access logs
ExtraLogFieldsCtxKey caddy.CtxKey = "extra_log_fields" ExtraLogFieldsCtxKey caddy.CtxKey = "extra_log_fields"
// Variable name used to indicate the logger to be used
AccessLoggerNameVarKey string = "access_logger_names"
) )

View file

@ -369,7 +369,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
errLog = errLog.With(zap.Duration("duration", duration)) errLog = errLog.With(zap.Duration("duration", duration))
errLoggers := []*zap.Logger{errLog} errLoggers := []*zap.Logger{errLog}
if s.Logs != nil { 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 // get the values that will be used to log the error
@ -778,7 +778,7 @@ func (s *Server) logRequest(
loggers := []*zap.Logger{accLog} loggers := []*zap.Logger{accLog}
if s.Logs != nil { 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 // 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, OriginalRequestCtxKey, originalRequest(r, &url2))
ctx = context.WithValue(ctx, ExtraLogFieldsCtxKey, new(ExtraLogFields)) ctx = context.WithValue(ctx, ExtraLogFieldsCtxKey, new(ExtraLogFields))
r = r.WithContext(ctx) r = r.WithContext(ctx)
// once the pointer to the request won't change // once the pointer to the request won't change