Skip to content

Commit

Permalink
httpcaddyfile: Allow hostnames & logger name overrides for log dire…
Browse files Browse the repository at this point in the history
…ctive (#5643)

* httpcaddyfile: Allow `hostnames` override for log directive

* Implement access logger name overrides

* Fix panic & default logger clobbering edgecase
  • Loading branch information
francislavoie authored Aug 2, 2023
1 parent da23501 commit 5c51c1d
Show file tree
Hide file tree
Showing 6 changed files with 342 additions and 29 deletions.
76 changes: 54 additions & 22 deletions caddyconfig/httpcaddyfile/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -788,7 +788,8 @@ func parseInvoke(h Helper) (caddyhttp.MiddlewareHandler, error) {

// parseLog parses the log directive. Syntax:
//
// log {
// log <logger_name> {
// hostnames <hostnames...>
// output <writer_module> ...
// format <encoder_module> ...
// level <level>
Expand All @@ -809,11 +810,13 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
var configValues []ConfigValue
for h.Next() {
// Logic below expects that a name is always present when a
// global option is being parsed.
var globalLogName string
// global option is being parsed; or an optional override
// is supported for access logs.
var logName string

if parseAsGlobalOption {
if h.NextArg() {
globalLogName = h.Val()
logName = h.Val()

// Only a single argument is supported.
if h.NextArg() {
Expand All @@ -824,26 +827,47 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
// reference the default logger. See the
// setupNewDefault function in the logging
// package for where this is configured.
globalLogName = caddy.DefaultLoggerName
logName = caddy.DefaultLoggerName
}

// Verify this name is unused.
_, used := globalLogNames[globalLogName]
_, used := globalLogNames[logName]
if used {
return nil, h.Err("duplicate global log option for: " + globalLogName)
return nil, h.Err("duplicate global log option for: " + logName)
}
globalLogNames[globalLogName] = struct{}{}
globalLogNames[logName] = struct{}{}
} else {
// No arguments are supported for the server block log directive
// An optional override of the logger name can be provided;
// otherwise a default will be used, like "log0", "log1", etc.
if h.NextArg() {
return nil, h.ArgErr()
logName = h.Val()

// Only a single argument is supported.
if h.NextArg() {
return nil, h.ArgErr()
}
}
}

cl := new(caddy.CustomLog)

// allow overriding the current site block's hostnames for this logger;
// this is useful for setting up loggers per subdomain in a site block
// with a wildcard domain
customHostnames := []string{}

for h.NextBlock(0) {
switch h.Val() {
case "hostnames":
if parseAsGlobalOption {
return nil, h.Err("hostnames is not allowed in the log global options")
}
args := h.RemainingArgs()
if len(args) == 0 {
return nil, h.ArgErr()
}
customHostnames = append(customHostnames, args...)

case "output":
if !h.NextArg() {
return nil, h.ArgErr()
Expand Down Expand Up @@ -902,18 +926,16 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
}

case "include":
// This configuration is only allowed in the global options
if !parseAsGlobalOption {
return nil, h.ArgErr()
return nil, h.Err("include is not allowed in the log directive")
}
for h.NextArg() {
cl.Include = append(cl.Include, h.Val())
}

case "exclude":
// This configuration is only allowed in the global options
if !parseAsGlobalOption {
return nil, h.ArgErr()
return nil, h.Err("exclude is not allowed in the log directive")
}
for h.NextArg() {
cl.Exclude = append(cl.Exclude, h.Val())
Expand All @@ -925,24 +947,34 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
}

var val namedCustomLog
val.hostnames = customHostnames

isEmptyConfig := reflect.DeepEqual(cl, new(caddy.CustomLog))

// Skip handling of empty logging configs
if !reflect.DeepEqual(cl, new(caddy.CustomLog)) {
if parseAsGlobalOption {
// Use indicated name for global log options
val.name = globalLogName
val.log = cl
} else {

if parseAsGlobalOption {
// Use indicated name for global log options
val.name = logName
} else {
if logName != "" {
val.name = logName
} else if !isEmptyConfig {
// Construct a log name for server log streams
logCounter, ok := h.State["logCounter"].(int)
if !ok {
logCounter = 0
}
val.name = fmt.Sprintf("log%d", logCounter)
cl.Include = []string{"http.log.access." + val.name}
val.log = cl
logCounter++
h.State["logCounter"] = logCounter
}
if val.name != "" {
cl.Include = []string{"http.log.access." + val.name}
}
}
if !isEmptyConfig {
val.log = cl
}
configValues = append(configValues, ConfigValue{
Class: "custom_log",
Expand Down
5 changes: 3 additions & 2 deletions caddyconfig/httpcaddyfile/builtins_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,13 @@ func TestLogDirectiveSyntax(t *testing.T) {
},
{
input: `:8080 {
log invalid {
log name-override {
output file foo.log
}
}
`,
expectError: true,
output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.name-override"]},"name-override":{"writer":{"filename":"foo.log","output":"file"},"include":["http.log.access.name-override"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"name-override"}}}}}}`,
expectError: false,
},
} {

Expand Down
42 changes: 37 additions & 5 deletions caddyconfig/httpcaddyfile/httptype.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"github.com/caddyserver/caddy/v2/modules/caddypki"
"github.com/caddyserver/caddy/v2/modules/caddytls"
"go.uber.org/zap"
"golang.org/x/exp/slices"
)

func init() {
Expand Down Expand Up @@ -241,7 +242,7 @@ func (st ServerType) Setup(
if ncl.name == caddy.DefaultLoggerName {
hasDefaultLog = true
}
if _, ok := options["debug"]; ok && ncl.log.Level == "" {
if _, ok := options["debug"]; ok && ncl.log != nil && ncl.log.Level == "" {
ncl.log.Level = zap.DebugLevel.CapitalString()
}
customLogs = append(customLogs, ncl)
Expand Down Expand Up @@ -324,7 +325,21 @@ func (st ServerType) Setup(
Logs: make(map[string]*caddy.CustomLog),
}
}

// Add the default log first if defined, so that it doesn't
// accidentally get re-created below due to the Exclude logic
for _, ncl := range customLogs {
if ncl.name == caddy.DefaultLoggerName && ncl.log != nil {
cfg.Logging.Logs[caddy.DefaultLoggerName] = ncl.log
break
}
}

// Add the rest of the custom logs
for _, ncl := range customLogs {
if ncl.log == nil || ncl.name == caddy.DefaultLoggerName {
continue
}
if ncl.name != "" {
cfg.Logging.Logs[ncl.name] = ncl.log
}
Expand All @@ -338,8 +353,16 @@ func (st ServerType) Setup(
cfg.Logging.Logs[caddy.DefaultLoggerName] = defaultLog
}
defaultLog.Exclude = append(defaultLog.Exclude, ncl.log.Include...)

// avoid duplicates by sorting + compacting
slices.Sort[string](defaultLog.Exclude)
defaultLog.Exclude = slices.Compact[[]string, string](defaultLog.Exclude)
}
}
// we may have not actually added anything, so remove if empty
if len(cfg.Logging.Logs) == 0 {
cfg.Logging = nil
}
}

return cfg, warnings, nil
Expand Down Expand Up @@ -770,12 +793,20 @@ func (st *ServerType) serversFromPairings(
sblockLogHosts := sblock.hostsFromKeys(true)
for _, cval := range sblock.pile["custom_log"] {
ncl := cval.Value.(namedCustomLog)
if sblock.hasHostCatchAllKey() {
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
srv.Logs.DefaultLoggerName = ncl.name
} else if len(ncl.hostnames) > 0 {
// if the logger overrides the hostnames, map that to the logger name
for _, h := range ncl.hostnames {
if srv.Logs.LoggerNames == nil {
srv.Logs.LoggerNames = make(map[string]string)
}
srv.Logs.LoggerNames[h] = ncl.name
}
} else {
// map each host to the user's desired logger name
// otherwise, map each host to the logger name
for _, h := range sblockLogHosts {
if srv.Logs.LoggerNames == nil {
srv.Logs.LoggerNames = make(map[string]string)
Expand Down Expand Up @@ -1564,8 +1595,9 @@ func (c counter) nextGroup() string {
}

type namedCustomLog struct {
name string
log *caddy.CustomLog
name string
hostnames []string
log *caddy.CustomLog
}

// sbAddrAssociation is a mapping from a list of
Expand Down
71 changes: 71 additions & 0 deletions caddytest/integration/caddyfile_adapt/log_override_hostname.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
*.example.com {
log {
hostnames foo.example.com bar.example.com
output file /foo-bar.txt
}
log {
hostnames baz.example.com
output file /baz.txt
}
}
----------
{
"logging": {
"logs": {
"default": {
"exclude": [
"http.log.access.log0",
"http.log.access.log1"
]
},
"log0": {
"writer": {
"filename": "/foo-bar.txt",
"output": "file"
},
"include": [
"http.log.access.log0"
]
},
"log1": {
"writer": {
"filename": "/baz.txt",
"output": "file"
},
"include": [
"http.log.access.log1"
]
}
}
},
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"*.example.com"
]
}
],
"terminal": true
}
],
"logs": {
"logger_names": {
"bar.example.com": "log0",
"baz.example.com": "log1",
"foo.example.com": "log0"
}
}
}
}
}
}
}
Loading

0 comments on commit 5c51c1d

Please sign in to comment.