diff --git a/caddyconfig/httpcaddyfile/builtins.go b/caddyconfig/httpcaddyfile/builtins.go index 8a8f3cc98b89..226ab8696c0c 100644 --- a/caddyconfig/httpcaddyfile/builtins.go +++ b/caddyconfig/httpcaddyfile/builtins.go @@ -612,18 +612,54 @@ func parseHandleErrors(h Helper) ([]ConfigValue, error) { // parseLog parses the log directive. Syntax: // -// log { -// output ... -// format ... -// level +// log [name] { +// output ... +// format ... +// level +// include +// exclude // } // +// When the name argument is unspecified, this directive modifies the HTTP +// access logs and adds an include for that namespace. +// +// Specifying the name "default" will modify the default logger used globally +// within Caddy. +// func parseLog(h Helper) ([]ConfigValue, error) { + return parseLogHelper(h, false) +} + +// parseLogHelper is used both for the parseLog directive within Server Blocks, +// as well as the global "log" option for configuring loggers at the global +// level. The globalOption parameter is used to distinguish any differing logic +// between the two. +func parseLogHelper(h Helper, globalOption bool) ([]ConfigValue, error) { var configValues []ConfigValue for h.Next() { - // log does not currently support any arguments - if h.NextArg() { - return nil, h.ArgErr() + // Logic below expects that a name is always when a global + // option is being parsed. + var globalLogName string + if globalOption { + if h.NextArg() { + // Allow for a log name to be specified, + globalLogName = h.Val() + + // Only a single argument is supported. + if h.NextArg() { + return nil, h.ArgErr() + } + } else { + // In the global case, if the log name is unset + // then we assume it refers to the default + // logger. + globalLogName = "default" + } + } else { + // No arguments are supported for the server block log directive + if h.NextArg() { + return nil, h.ArgErr() + } } cl := new(caddy.CustomLog) @@ -687,22 +723,46 @@ func parseLog(h Helper) ([]ConfigValue, error) { return nil, h.ArgErr() } + case "include": + if !h.NextArg() { + return nil, h.ArgErr() + } + for h.NextArg() { + cl.Include = append(cl.Include, h.Val()) + } + + case "exclude": + if !h.NextArg() { + return nil, h.ArgErr() + } + for h.NextArg() { + cl.Exclude = append(cl.Exclude, h.Val()) + } + default: return nil, h.Errf("unrecognized subdirective: %s", h.Val()) } } var val namedCustomLog + // Skip handling of empty logging configs if !reflect.DeepEqual(cl, new(caddy.CustomLog)) { - logCounter, ok := h.State["logCounter"].(int) - if !ok { - logCounter = 0 + if globalOption { + // Use indicated name for global log options + val.name = globalLogName + val.log = cl + } else { + // 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 } - val.name = fmt.Sprintf("log%d", logCounter) - cl.Include = []string{"http.log.access." + val.name} - val.log = cl - logCounter++ - h.State["logCounter"] = logCounter } configValues = append(configValues, ConfigValue{ Class: "custom_log", diff --git a/caddyconfig/httpcaddyfile/builtins_test.go b/caddyconfig/httpcaddyfile/builtins_test.go index 09bb4ed280e6..496238301688 100644 --- a/caddyconfig/httpcaddyfile/builtins_test.go +++ b/caddyconfig/httpcaddyfile/builtins_test.go @@ -10,6 +10,7 @@ import ( func TestLogDirectiveSyntax(t *testing.T) { for i, tc := range []struct { input string + output string expectError bool }{ { @@ -17,6 +18,7 @@ func TestLogDirectiveSyntax(t *testing.T) { log } `, + output: `{"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{}}}}}}`, expectError: false, }, { @@ -26,11 +28,31 @@ func TestLogDirectiveSyntax(t *testing.T) { } } `, + output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"writer":{"filename":"foo.log","output":"file"},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`, expectError: false, }, { input: `:8080 { - log /foo { + log { + format filter { + wrap console + fields { + common_log delete + request>remote_addr ip_mask { + ipv4 24 + ipv6 32 + } + } + } + } + } + `, + output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"encoder":{"fields":{"common_log":{"filter":"delete"},"request\u003eremote_addr":{"filter":"ip_mask","ipv4_cidr":24,"ipv6_cidr":32}},"format":"filter","wrap":{"format":"console"}},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`, + expectError: false, + }, + { + input: `:8080 { + log invalid { output file foo.log } } @@ -43,12 +65,16 @@ func TestLogDirectiveSyntax(t *testing.T) { ServerType: ServerType{}, } - _, _, err := adapter.Adapt([]byte(tc.input), nil) + out, _, err := adapter.Adapt([]byte(tc.input), nil) if err != nil != tc.expectError { t.Errorf("Test %d error expectation failed Expected: %v, got %s", i, tc.expectError, err) continue } + + if string(out) != tc.output { + t.Errorf("Test %d error output mismatch Expected: %s, got %s", i, tc.output, out) + } } } diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go index 3f638c934062..b5115edc470d 100644 --- a/caddyconfig/httpcaddyfile/httptype.go +++ b/caddyconfig/httpcaddyfile/httptype.go @@ -240,20 +240,29 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock, // extract any custom logs, and enforce configured levels var customLogs []namedCustomLog var hasDefaultLog bool + addCustomLog := func(ncl namedCustomLog) { + if ncl.name == "" { + return + } + if ncl.name == "default" { + hasDefaultLog = true + } + if _, ok := options["debug"]; ok && ncl.log.Level == "" { + ncl.log.Level = "DEBUG" + } + customLogs = append(customLogs, ncl) + } + // Apply global log options, when set + if options["log"] != nil { + for _, logValue := range options["log"].([]ConfigValue) { + addCustomLog(logValue.Value.(namedCustomLog)) + } + } + // Apply server-specific log options for _, p := range pairings { for _, sb := range p.serverBlocks { for _, clVal := range sb.pile["custom_log"] { - ncl := clVal.Value.(namedCustomLog) - if ncl.name == "" { - continue - } - if ncl.name == "default" { - hasDefaultLog = true - } - if _, ok := options["debug"]; ok && ncl.log.Level == "" { - ncl.log.Level = "DEBUG" - } - customLogs = append(customLogs, ncl) + addCustomLog(clVal.Value.(namedCustomLog)) } } } diff --git a/caddyconfig/httpcaddyfile/options.go b/caddyconfig/httpcaddyfile/options.go index 54672a65d7b0..002613a43276 100644 --- a/caddyconfig/httpcaddyfile/options.go +++ b/caddyconfig/httpcaddyfile/options.go @@ -18,6 +18,7 @@ import ( "strconv" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/modules/caddytls" "github.com/caddyserver/certmagic" @@ -44,6 +45,7 @@ func init() { RegisterGlobalOption("auto_https", parseOptAutoHTTPS) RegisterGlobalOption("servers", parseServerOptions) RegisterGlobalOption("ocsp_stapling", parseOCSPStaplingOptions) + RegisterGlobalOption("log", parseLogOptions) } func parseOptTrue(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { return true, nil } @@ -385,3 +387,33 @@ func parseOCSPStaplingOptions(d *caddyfile.Dispenser, _ interface{}) (interface{ DisableStapling: val == "off", }, nil } + +// parseLogOptions parses the global log option. Syntax: +// +// log [name] { +// output ... +// format ... +// level +// include +// exclude +// } +// +// When the name argument is unspecified, this directive modifies the default +// logger. +// +func parseLogOptions(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { + var warnings []caddyconfig.Warning + // Call out the same parser that handles server-specific log configuration. + configValues, err := parseLogHelper(Helper{ + Dispenser: d, + warnings: &warnings, + }, true) + if err != nil { + return nil, err + } + if len(warnings) > 0 { + return nil, d.Errf("warnings found in parsing global log options: %+v", warnings) + } + + return configValues, nil +} diff --git a/caddyconfig/httpcaddyfile/options_test.go b/caddyconfig/httpcaddyfile/options_test.go new file mode 100644 index 000000000000..2387f9be2313 --- /dev/null +++ b/caddyconfig/httpcaddyfile/options_test.go @@ -0,0 +1,92 @@ +package httpcaddyfile + +import ( + "testing" + + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + _ "github.com/caddyserver/caddy/v2/modules/logging" +) + +func TestGlobalLogOptionSyntax(t *testing.T) { + for i, tc := range []struct { + input string + output string + expectError bool + }{ + { + input: `{ + log + } + `, + output: `{}`, + expectError: false, + }, + { + input: `{ + log { + output file foo.log + } + } + `, + output: `{"logging":{"logs":{"default":{"writer":{"filename":"foo.log","output":"file"}}}}}`, + expectError: false, + }, + { + input: `{ + log { + format filter { + wrap console + fields { + common_log delete + request>remote_addr ip_mask { + ipv4 24 + ipv6 32 + } + } + } + } + } + `, + output: `{"logging":{"logs":{"default":{"encoder":{"fields":{"common_log":{"filter":"delete"},"request\u003eremote_addr":{"filter":"ip_mask","ipv4_cidr":24,"ipv6_cidr":32}},"format":"filter","wrap":{"format":"console"}}}}}}`, + expectError: false, + }, + { + input: `{ + log { + output file foo.log + } + log default { + format json + } + } + `, + output: `{"logging":{"logs":{"default":{"encoder":{"format":"json"}}}}}`, + expectError: false, + }, + { + input: `{ + log example /foo { + output file foo.log + } + } + `, + expectError: true, + }, + } { + + adapter := caddyfile.Adapter{ + ServerType: ServerType{}, + } + + out, _, err := adapter.Adapt([]byte(tc.input), nil) + + if err != nil != tc.expectError { + t.Errorf("Test %d error expectation failed Expected: %v, got %v", i, tc.expectError, err) + continue + } + + if string(out) != tc.output { + t.Errorf("Test %d error output mismatch Expected: %s, got %s", i, tc.output, out) + } + } +}