@marcel91200 / caddy Mirror · read-only / commits / 2a127ac3
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 14, 05:15 UTC

caddyconfig: add global option for configuring loggers (#4028)

This change is aimed at enhancing the logging module within the Caddyfile directive to allow users to configure logs other than the HTTP access log stream, which is the current capability of the Caddyfile [1]. The intent here is to leverage the same syntax as the server log directive at a global level, so that similar customizations can be added without needing to resort to a JSON-based configuration. Discussion for this approach happened in the referenced issue. Closes https://github.com/caddyserver/caddy/issues/3958 [1] https://caddyserver.com/docs/caddyfile/directives/log

Aaron Taylor committed Mar 12, 2021 at 15:00 UTC 2a127ac3d1650052f39e93f277e1dfd8c7c730e4
9 files changed +414 -26
caddyconfig/httpcaddyfile/builtins.go
+76 -11
@@ -619,11 +619,50 @@ func parseHandleErrors(h Helper) ([]ConfigValue, error) {
619 // }
620 //
621 func parseLog(h Helper) ([]ConfigValue, error) {
622 + return parseLogHelper(h, nil)
623 +}
624 +
625 +// parseLogHelper is used both for the parseLog directive within Server Blocks,
626 +// as well as the global "log" option for configuring loggers at the global
627 +// level. The parseAsGlobalOption parameter is used to distinguish any differing logic
628 +// between the two.
629 +func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue, error) {
630 + // When the globalLogNames parameter is passed in, we make
631 + // modifications to the parsing behavior.
632 + parseAsGlobalOption := globalLogNames != nil
633 +
634 var configValues []ConfigValue
635 for h.Next() {
624 - // log does not currently support any arguments
625 - if h.NextArg() {
626 - return nil, h.ArgErr()
636 + // Logic below expects that a name is always present when a
637 + // global option is being parsed.
638 + var globalLogName string
639 + if parseAsGlobalOption {
640 + if h.NextArg() {
641 + globalLogName = h.Val()
642 +
643 + // Only a single argument is supported.
644 + if h.NextArg() {
645 + return nil, h.ArgErr()
646 + }
647 + } else {
648 + // If there is no log name specified, we
649 + // reference the default logger. See the
650 + // setupNewDefault function in the logging
651 + // package for where this is configured.
652 + globalLogName = "default"
653 + }
654 +
655 + // Verify this name is unused.
656 + _, used := globalLogNames[globalLogName]
657 + if used {
658 + return nil, h.Err("duplicate global log option for: " + globalLogName)
659 + }
660 + globalLogNames[globalLogName] = struct{}{}
661 + } else {
662 + // No arguments are supported for the server block log directive
663 + if h.NextArg() {
664 + return nil, h.ArgErr()
665 + }
666 }
667
668 cl := new(caddy.CustomLog)
@@ -687,22 +726,48 @@ func parseLog(h Helper) ([]ConfigValue, error) {
726 return nil, h.ArgErr()
727 }
728
729 + case "include":
730 + // This configuration is only allowed in the global options
731 + if !parseAsGlobalOption {
732 + return nil, h.ArgErr()
733 + }
734 + for h.NextArg() {
735 + cl.Include = append(cl.Include, h.Val())
736 + }
737 +
738 + case "exclude":
739 + // This configuration is only allowed in the global options
740 + if !parseAsGlobalOption {
741 + return nil, h.ArgErr()
742 + }
743 + for h.NextArg() {
744 + cl.Exclude = append(cl.Exclude, h.Val())
745 + }
746 +
747 default:
748 return nil, h.Errf("unrecognized subdirective: %s", h.Val())
749 }
750 }
751
752 var val namedCustomLog
753 + // Skip handling of empty logging configs
754 if !reflect.DeepEqual(cl, new(caddy.CustomLog)) {
697 - logCounter, ok := h.State["logCounter"].(int)
698 - if !ok {
699 - logCounter = 0
755 + if parseAsGlobalOption {
756 + // Use indicated name for global log options
757 + val.name = globalLogName
758 + val.log = cl
759 + } else {
760 + // Construct a log name for server log streams
761 + logCounter, ok := h.State["logCounter"].(int)
762 + if !ok {
763 + logCounter = 0
764 + }
765 + val.name = fmt.Sprintf("log%d", logCounter)
766 + cl.Include = []string{"http.log.access." + val.name}
767 + val.log = cl
768 + logCounter++
769 + h.State["logCounter"] = logCounter
770 }
701 - val.name = fmt.Sprintf("log%d", logCounter)
702 - cl.Include = []string{"http.log.access." + val.name}
703 - val.log = cl
704 - logCounter++
705 - h.State["logCounter"] = logCounter
771 }
772 configValues = append(configValues, ConfigValue{
773 Class: "custom_log",
caddyconfig/httpcaddyfile/builtins_test.go
+28 -2
@@ -10,6 +10,7 @@ import (
10 func TestLogDirectiveSyntax(t *testing.T) {
11 for i, tc := range []struct {
12 input string
13 + output string
14 expectError bool
15 }{
16 {
@@ -17,6 +18,7 @@ func TestLogDirectiveSyntax(t *testing.T) {
18 log
19 }
20 `,
21 + output: `{"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{}}}}}}`,
22 expectError: false,
23 },
24 {
@@ -26,11 +28,31 @@ func TestLogDirectiveSyntax(t *testing.T) {
28 }
29 }
30 `,
31 + 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"}}}}}}`,
32 expectError: false,
33 },
34 {
35 input: `:8080 {
33 - log /foo {
36 + log {
37 + format filter {
38 + wrap console
39 + fields {
40 + common_log delete
41 + request>remote_addr ip_mask {
42 + ipv4 24
43 + ipv6 32
44 + }
45 + }
46 + }
47 + }
48 + }
49 + `,
50 + 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"}}}}}}`,
51 + expectError: false,
52 + },
53 + {
54 + input: `:8080 {
55 + log invalid {
56 output file foo.log
57 }
58 }
@@ -43,12 +65,16 @@ func TestLogDirectiveSyntax(t *testing.T) {
65 ServerType: ServerType{},
66 }
67
46 - _, _, err := adapter.Adapt([]byte(tc.input), nil)
68 + out, _, err := adapter.Adapt([]byte(tc.input), nil)
69
70 if err != nil != tc.expectError {
71 t.Errorf("Test %d error expectation failed Expected: %v, got %s", i, tc.expectError, err)
72 continue
73 }
74 +
75 + if string(out) != tc.output {
76 + t.Errorf("Test %d error output mismatch Expected: %s, got %s", i, tc.output, out)
77 + }
78 }
79 }
80
caddyconfig/httpcaddyfile/httptype.go
+36 -13
@@ -240,20 +240,29 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
240 // extract any custom logs, and enforce configured levels
241 var customLogs []namedCustomLog
242 var hasDefaultLog bool
243 + addCustomLog := func(ncl namedCustomLog) {
244 + if ncl.name == "" {
245 + return
246 + }
247 + if ncl.name == "default" {
248 + hasDefaultLog = true
249 + }
250 + if _, ok := options["debug"]; ok && ncl.log.Level == "" {
251 + ncl.log.Level = "DEBUG"
252 + }
253 + customLogs = append(customLogs, ncl)
254 + }
255 + // Apply global log options, when set
256 + if options["log"] != nil {
257 + for _, logValue := range options["log"].([]ConfigValue) {
258 + addCustomLog(logValue.Value.(namedCustomLog))
259 + }
260 + }
261 + // Apply server-specific log options
262 for _, p := range pairings {
263 for _, sb := range p.serverBlocks {
264 for _, clVal := range sb.pile["custom_log"] {
246 - ncl := clVal.Value.(namedCustomLog)
247 - if ncl.name == "" {
248 - continue
249 - }
250 - if ncl.name == "default" {
251 - hasDefaultLog = true
252 - }
253 - if _, ok := options["debug"]; ok && ncl.log.Level == "" {
254 - ncl.log.Level = "DEBUG"
255 - }
256 - customLogs = append(customLogs, ncl)
265 + addCustomLog(clVal.Value.(namedCustomLog))
266 }
267 }
268 }
@@ -313,7 +322,7 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
322 // most users seem to prefer not writing access logs
323 // to the default log when they are directed to a
324 // file or have any other special customization
316 - if len(ncl.log.Include) > 0 {
325 + if ncl.name != "default" && len(ncl.log.Include) > 0 {
326 defaultLog, ok := cfg.Logging.Logs["default"]
327 if !ok {
328 defaultLog = new(caddy.CustomLog)
@@ -362,11 +371,25 @@ func (ServerType) evaluateGlobalOptionsBlock(serverBlocks []serverBlock, options
371 }
372 serverOpts, ok := val.(serverOptions)
373 if !ok {
365 - return nil, fmt.Errorf("unexpected type from 'servers' global options")
374 + return nil, fmt.Errorf("unexpected type from 'servers' global options: %T", val)
375 }
376 options[opt] = append(existingOpts, serverOpts)
377 continue
378 }
379 + // Additionally, fold multiple "log" options together into an
380 + // array so that multiple loggers can be configured.
381 + if opt == "log" {
382 + existingOpts, ok := options[opt].([]ConfigValue)
383 + if !ok {
384 + existingOpts = []ConfigValue{}
385 + }
386 + logOpts, ok := val.([]ConfigValue)
387 + if !ok {
388 + return nil, fmt.Errorf("unexpected type from 'log' global options: %T", val)
389 + }
390 + options[opt] = append(existingOpts, logOpts...)
391 + continue
392 + }
393
394 options[opt] = val
395 }
caddyconfig/httpcaddyfile/options.go
+50
@@ -18,6 +18,7 @@ import (
18 "strconv"
19
20 "github.com/caddyserver/caddy/v2"
21 + "github.com/caddyserver/caddy/v2/caddyconfig"
22 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
23 "github.com/caddyserver/caddy/v2/modules/caddytls"
24 "github.com/caddyserver/certmagic"
@@ -44,6 +45,7 @@ func init() {
45 RegisterGlobalOption("auto_https", parseOptAutoHTTPS)
46 RegisterGlobalOption("servers", parseServerOptions)
47 RegisterGlobalOption("ocsp_stapling", parseOCSPStaplingOptions)
48 + RegisterGlobalOption("log", parseLogOptions)
49 }
50
51 func parseOptTrue(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { return true, nil }
@@ -385,3 +387,51 @@ func parseOCSPStaplingOptions(d *caddyfile.Dispenser, _ interface{}) (interface{
387 DisableStapling: val == "off",
388 }, nil
389 }
390 +
391 +// parseLogOptions parses the global log option. Syntax:
392 +//
393 +// log [name] {
394 +// output <writer_module> ...
395 +// format <encoder_module> ...
396 +// level <level>
397 +// include <namespaces...>
398 +// exclude <namespaces...>
399 +// }
400 +//
401 +// When the name argument is unspecified, this directive modifies the default
402 +// logger.
403 +//
404 +func parseLogOptions(d *caddyfile.Dispenser, existingVal interface{}) (interface{}, error) {
405 + currentNames := make(map[string]struct{})
406 + if existingVal != nil {
407 + innerVals, ok := existingVal.([]ConfigValue)
408 + if !ok {
409 + return nil, d.Errf("existing log values of unexpected type: %T", existingVal)
410 + }
411 + for _, rawVal := range innerVals {
412 + val, ok := rawVal.Value.(namedCustomLog)
413 + if !ok {
414 + return nil, d.Errf("existing log value of unexpected type: %T", existingVal)
415 + }
416 + currentNames[val.name] = struct{}{}
417 + }
418 + }
419 +
420 + var warnings []caddyconfig.Warning
421 + // Call out the same parser that handles server-specific log configuration.
422 + configValues, err := parseLogHelper(
423 + Helper{
424 + Dispenser: d,
425 + warnings: &warnings,
426 + },
427 + currentNames,
428 + )
429 + if err != nil {
430 + return nil, err
431 + }
432 + if len(warnings) > 0 {
433 + return nil, d.Errf("warnings found in parsing global log options: %+v", warnings)
434 + }
435 +
436 + return configValues, nil
437 +}
caddyconfig/httpcaddyfile/options_test.go new
+64
@@ -0,0 +1,64 @@
1 +package httpcaddyfile
2 +
3 +import (
4 + "testing"
5 +
6 + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
7 + _ "github.com/caddyserver/caddy/v2/modules/logging"
8 +)
9 +
10 +func TestGlobalLogOptionSyntax(t *testing.T) {
11 + for i, tc := range []struct {
12 + input string
13 + output string
14 + expectError bool
15 + }{
16 + // NOTE: Additional test cases of successful Caddyfile parsing
17 + // are present in: caddytest/integration/caddyfile_adapt/
18 + {
19 + input: `{
20 + log default
21 + }
22 + `,
23 + output: `{}`,
24 + expectError: false,
25 + },
26 + {
27 + input: `{
28 + log example {
29 + output file foo.log
30 + }
31 + log example {
32 + format json
33 + }
34 + }
35 + `,
36 + expectError: true,
37 + },
38 + {
39 + input: `{
40 + log example /foo {
41 + output file foo.log
42 + }
43 + }
44 + `,
45 + expectError: true,
46 + },
47 + } {
48 +
49 + adapter := caddyfile.Adapter{
50 + ServerType: ServerType{},
51 + }
52 +
53 + out, _, err := adapter.Adapt([]byte(tc.input), nil)
54 +
55 + if err != nil != tc.expectError {
56 + t.Errorf("Test %d error expectation failed Expected: %v, got %v", i, tc.expectError, err)
57 + continue
58 + }
59 +
60 + if string(out) != tc.output {
61 + t.Errorf("Test %d error output mismatch Expected: %s, got %s", i, tc.output, out)
62 + }
63 + }
64 +}
caddytest/integration/caddyfile_adapt/global_options_log_and_site.txt new
+77
@@ -0,0 +1,77 @@
1 +{
2 + log {
3 + output file caddy.log
4 + include some-log-source
5 + exclude admin.api admin2.api
6 + }
7 + log custom-logger {
8 + output file caddy.log
9 + level WARN
10 + include custom-log-source
11 + }
12 +}
13 +
14 +:8884 {
15 + log {
16 + format json
17 + output file access.log
18 + }
19 +}
20 +----------
21 +{
22 + "logging": {
23 + "logs": {
24 + "custom-logger": {
25 + "writer": {
26 + "filename": "caddy.log",
27 + "output": "file"
28 + },
29 + "level": "WARN",
30 + "include": [
31 + "custom-log-source"
32 + ]
33 + },
34 + "default": {
35 + "writer": {
36 + "filename": "caddy.log",
37 + "output": "file"
38 + },
39 + "include": [
40 + "some-log-source"
41 + ],
42 + "exclude": [
43 + "admin.api",
44 + "admin2.api",
45 + "custom-log-source",
46 + "http.log.access.log0"
47 + ]
48 + },
49 + "log0": {
50 + "writer": {
51 + "filename": "access.log",
52 + "output": "file"
53 + },
54 + "encoder": {
55 + "format": "json"
56 + },
57 + "include": [
58 + "http.log.access.log0"
59 + ]
60 + }
61 + }
62 + },
63 + "apps": {
64 + "http": {
65 + "servers": {
66 + "srv0": {
67 + "listen": [
68 + ":8884"
69 + ],
70 + "logs": {
71 + "default_logger_name": "log0"
72 + }
73 + }
74 + }
75 + }
76 + }
77 +}
caddytest/integration/caddyfile_adapt/global_options_log_basic.txt new
+18
@@ -0,0 +1,18 @@
1 +{
2 + log {
3 + output file foo.log
4 + }
5 +}
6 +----------
7 +{
8 + "logging": {
9 + "logs": {
10 + "default": {
11 + "writer": {
12 + "filename": "foo.log",
13 + "output": "file"
14 + }
15 + }
16 + }
17 + }
18 +}
caddytest/integration/caddyfile_adapt/global_options_log_custom.txt new
+39
@@ -0,0 +1,39 @@
1 +{
2 + log custom-logger {
3 + format filter {
4 + wrap console
5 + fields {
6 + common_log delete
7 + request>remote_addr ip_mask {
8 + ipv4 24
9 + ipv6 32
10 + }
11 + }
12 + }
13 + }
14 +}
15 +----------
16 +{
17 + "logging": {
18 + "logs": {
19 + "custom-logger": {
20 + "encoder": {
21 + "fields": {
22 + "common_log": {
23 + "filter": "delete"
24 + },
25 + "request\u003eremote_addr": {
26 + "filter": "ip_mask",
27 + "ipv4_cidr": 24,
28 + "ipv6_cidr": 32
29 + }
30 + },
31 + "format": "filter",
32 + "wrap": {
33 + "format": "console"
34 + }
35 + }
36 + }
37 + }
38 + }
39 +}
caddytest/integration/caddyfile_adapt/global_options_log_multi.txt new
+26
@@ -0,0 +1,26 @@
1 +{
2 + log first {
3 + output file foo.log
4 + }
5 + log second {
6 + format json
7 + }
8 +}
9 +----------
10 +{
11 + "logging": {
12 + "logs": {
13 + "first": {
14 + "writer": {
15 + "filename": "foo.log",
16 + "output": "file"
17 + }
18 + },
19 + "second": {
20 + "encoder": {
21 + "format": "json"
22 + }
23 + }
24 + }
25 + }
26 +}