@marcel91200 / caddy Mirror · read-only / commits / 5c51c1db
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 13, 00:30 UTC

httpcaddyfile: Allow `hostnames` & logger name overrides for log directive (#5643)

* httpcaddyfile: Allow `hostnames` override for log directive * Implement access logger name overrides * Fix panic & default logger clobbering edgecase

Francis Lavoie committed Aug 2, 2023 at 03:13 UTC 5c51c1db2ce450a3fa003834097ad010b3844673
6 files changed +342 -29
caddyconfig/httpcaddyfile/builtins.go
+54 -22
@@ -788,7 +788,8 @@ func parseInvoke(h Helper) (caddyhttp.MiddlewareHandler, error) {
788
789 // parseLog parses the log directive. Syntax:
790 //
791 -// log {
791 +// log <logger_name> {
792 +// hostnames <hostnames...>
793 // output <writer_module> ...
794 // format <encoder_module> ...
795 // level <level>
@@ -809,11 +810,13 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
810 var configValues []ConfigValue
811 for h.Next() {
812 // Logic below expects that a name is always present when a
812 - // global option is being parsed.
813 - var globalLogName string
813 + // global option is being parsed; or an optional override
814 + // is supported for access logs.
815 + var logName string
816 +
817 if parseAsGlobalOption {
818 if h.NextArg() {
816 - globalLogName = h.Val()
819 + logName = h.Val()
820
821 // Only a single argument is supported.
822 if h.NextArg() {
@@ -824,26 +827,47 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
827 // reference the default logger. See the
828 // setupNewDefault function in the logging
829 // package for where this is configured.
827 - globalLogName = caddy.DefaultLoggerName
830 + logName = caddy.DefaultLoggerName
831 }
832
833 // Verify this name is unused.
831 - _, used := globalLogNames[globalLogName]
834 + _, used := globalLogNames[logName]
835 if used {
833 - return nil, h.Err("duplicate global log option for: " + globalLogName)
836 + return nil, h.Err("duplicate global log option for: " + logName)
837 }
835 - globalLogNames[globalLogName] = struct{}{}
838 + globalLogNames[logName] = struct{}{}
839 } else {
837 - // No arguments are supported for the server block log directive
840 + // An optional override of the logger name can be provided;
841 + // otherwise a default will be used, like "log0", "log1", etc.
842 if h.NextArg() {
839 - return nil, h.ArgErr()
843 + logName = h.Val()
844 +
845 + // Only a single argument is supported.
846 + if h.NextArg() {
847 + return nil, h.ArgErr()
848 + }
849 }
850 }
851
852 cl := new(caddy.CustomLog)
853
854 + // allow overriding the current site block's hostnames for this logger;
855 + // this is useful for setting up loggers per subdomain in a site block
856 + // with a wildcard domain
857 + customHostnames := []string{}
858 +
859 for h.NextBlock(0) {
860 switch h.Val() {
861 + case "hostnames":
862 + if parseAsGlobalOption {
863 + return nil, h.Err("hostnames is not allowed in the log global options")
864 + }
865 + args := h.RemainingArgs()
866 + if len(args) == 0 {
867 + return nil, h.ArgErr()
868 + }
869 + customHostnames = append(customHostnames, args...)
870 +
871 case "output":
872 if !h.NextArg() {
873 return nil, h.ArgErr()
@@ -902,18 +926,16 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
926 }
927
928 case "include":
905 - // This configuration is only allowed in the global options
929 if !parseAsGlobalOption {
907 - return nil, h.ArgErr()
930 + return nil, h.Err("include is not allowed in the log directive")
931 }
932 for h.NextArg() {
933 cl.Include = append(cl.Include, h.Val())
934 }
935
936 case "exclude":
914 - // This configuration is only allowed in the global options
937 if !parseAsGlobalOption {
916 - return nil, h.ArgErr()
938 + return nil, h.Err("exclude is not allowed in the log directive")
939 }
940 for h.NextArg() {
941 cl.Exclude = append(cl.Exclude, h.Val())
@@ -925,24 +947,34 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
947 }
948
949 var val namedCustomLog
950 + val.hostnames = customHostnames
951 +
952 + isEmptyConfig := reflect.DeepEqual(cl, new(caddy.CustomLog))
953 +
954 // Skip handling of empty logging configs
929 - if !reflect.DeepEqual(cl, new(caddy.CustomLog)) {
930 - if parseAsGlobalOption {
931 - // Use indicated name for global log options
932 - val.name = globalLogName
933 - val.log = cl
934 - } else {
955 +
956 + if parseAsGlobalOption {
957 + // Use indicated name for global log options
958 + val.name = logName
959 + } else {
960 + if logName != "" {
961 + val.name = logName
962 + } else if !isEmptyConfig {
963 // Construct a log name for server log streams
964 logCounter, ok := h.State["logCounter"].(int)
965 if !ok {
966 logCounter = 0
967 }
968 val.name = fmt.Sprintf("log%d", logCounter)
941 - cl.Include = []string{"http.log.access." + val.name}
942 - val.log = cl
969 logCounter++
970 h.State["logCounter"] = logCounter
971 }
972 + if val.name != "" {
973 + cl.Include = []string{"http.log.access." + val.name}
974 + }
975 + }
976 + if !isEmptyConfig {
977 + val.log = cl
978 }
979 configValues = append(configValues, ConfigValue{
980 Class: "custom_log",
caddyconfig/httpcaddyfile/builtins_test.go
+3 -2
@@ -52,12 +52,13 @@ func TestLogDirectiveSyntax(t *testing.T) {
52 },
53 {
54 input: `:8080 {
55 - log invalid {
55 + log name-override {
56 output file foo.log
57 }
58 }
59 `,
60 - expectError: true,
60 + 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"}}}}}}`,
61 + expectError: false,
62 },
63 } {
64
caddyconfig/httpcaddyfile/httptype.go
+37 -5
@@ -30,6 +30,7 @@ import (
30 "github.com/caddyserver/caddy/v2/modules/caddypki"
31 "github.com/caddyserver/caddy/v2/modules/caddytls"
32 "go.uber.org/zap"
33 + "golang.org/x/exp/slices"
34 )
35
36 func init() {
@@ -241,7 +242,7 @@ func (st ServerType) Setup(
242 if ncl.name == caddy.DefaultLoggerName {
243 hasDefaultLog = true
244 }
244 - if _, ok := options["debug"]; ok && ncl.log.Level == "" {
245 + if _, ok := options["debug"]; ok && ncl.log != nil && ncl.log.Level == "" {
246 ncl.log.Level = zap.DebugLevel.CapitalString()
247 }
248 customLogs = append(customLogs, ncl)
@@ -324,7 +325,21 @@ func (st ServerType) Setup(
325 Logs: make(map[string]*caddy.CustomLog),
326 }
327 }
328 +
329 + // Add the default log first if defined, so that it doesn't
330 + // accidentally get re-created below due to the Exclude logic
331 + for _, ncl := range customLogs {
332 + if ncl.name == caddy.DefaultLoggerName && ncl.log != nil {
333 + cfg.Logging.Logs[caddy.DefaultLoggerName] = ncl.log
334 + break
335 + }
336 + }
337 +
338 + // Add the rest of the custom logs
339 for _, ncl := range customLogs {
340 + if ncl.log == nil || ncl.name == caddy.DefaultLoggerName {
341 + continue
342 + }
343 if ncl.name != "" {
344 cfg.Logging.Logs[ncl.name] = ncl.log
345 }
@@ -338,8 +353,16 @@ func (st ServerType) Setup(
353 cfg.Logging.Logs[caddy.DefaultLoggerName] = defaultLog
354 }
355 defaultLog.Exclude = append(defaultLog.Exclude, ncl.log.Include...)
356 +
357 + // avoid duplicates by sorting + compacting
358 + slices.Sort[string](defaultLog.Exclude)
359 + defaultLog.Exclude = slices.Compact[[]string, string](defaultLog.Exclude)
360 }
361 }
362 + // we may have not actually added anything, so remove if empty
363 + if len(cfg.Logging.Logs) == 0 {
364 + cfg.Logging = nil
365 + }
366 }
367
368 return cfg, warnings, nil
@@ -770,12 +793,20 @@ func (st *ServerType) serversFromPairings(
793 sblockLogHosts := sblock.hostsFromKeys(true)
794 for _, cval := range sblock.pile["custom_log"] {
795 ncl := cval.Value.(namedCustomLog)
773 - if sblock.hasHostCatchAllKey() {
796 + if sblock.hasHostCatchAllKey() && len(ncl.hostnames) == 0 {
797 // all requests for hosts not able to be listed should use
798 // this log because it's a catch-all-hosts server block
799 srv.Logs.DefaultLoggerName = ncl.name
800 + } else if len(ncl.hostnames) > 0 {
801 + // if the logger overrides the hostnames, map that to the logger name
802 + for _, h := range ncl.hostnames {
803 + if srv.Logs.LoggerNames == nil {
804 + srv.Logs.LoggerNames = make(map[string]string)
805 + }
806 + srv.Logs.LoggerNames[h] = ncl.name
807 + }
808 } else {
778 - // map each host to the user's desired logger name
809 + // otherwise, map each host to the logger name
810 for _, h := range sblockLogHosts {
811 if srv.Logs.LoggerNames == nil {
812 srv.Logs.LoggerNames = make(map[string]string)
@@ -1564,8 +1595,9 @@ func (c counter) nextGroup() string {
1595 }
1596
1597 type namedCustomLog struct {
1567 - name string
1568 - log *caddy.CustomLog
1598 + name string
1599 + hostnames []string
1600 + log *caddy.CustomLog
1601 }
1602
1603 // sbAddrAssociation is a mapping from a list of
caddytest/integration/caddyfile_adapt/log_override_hostname.txt new
+71
@@ -0,0 +1,71 @@
1 +*.example.com {
2 + log {
3 + hostnames foo.example.com bar.example.com
4 + output file /foo-bar.txt
5 + }
6 + log {
7 + hostnames baz.example.com
8 + output file /baz.txt
9 + }
10 +}
11 +----------
12 +{
13 + "logging": {
14 + "logs": {
15 + "default": {
16 + "exclude": [
17 + "http.log.access.log0",
18 + "http.log.access.log1"
19 + ]
20 + },
21 + "log0": {
22 + "writer": {
23 + "filename": "/foo-bar.txt",
24 + "output": "file"
25 + },
26 + "include": [
27 + "http.log.access.log0"
28 + ]
29 + },
30 + "log1": {
31 + "writer": {
32 + "filename": "/baz.txt",
33 + "output": "file"
34 + },
35 + "include": [
36 + "http.log.access.log1"
37 + ]
38 + }
39 + }
40 + },
41 + "apps": {
42 + "http": {
43 + "servers": {
44 + "srv0": {
45 + "listen": [
46 + ":443"
47 + ],
48 + "routes": [
49 + {
50 + "match": [
51 + {
52 + "host": [
53 + "*.example.com"
54 + ]
55 + }
56 + ],
57 + "terminal": true
58 + }
59 + ],
60 + "logs": {
61 + "logger_names": {
62 + "bar.example.com": "log0",
63 + "baz.example.com": "log1",
64 + "foo.example.com": "log0"
65 + }
66 + }
67 + }
68 + }
69 + }
70 + }
71 +}
\ No newline at end of file
caddytest/integration/caddyfile_adapt/log_override_name_multiaccess.txt new
+86
@@ -0,0 +1,86 @@
1 +{
2 + log access-console {
3 + include http.log.access.foo
4 + output file access-localhost.log
5 + format console
6 + }
7 +
8 + log access-json {
9 + include http.log.access.foo
10 + output file access-localhost.json
11 + format json
12 + }
13 +}
14 +
15 +http://localhost:8881 {
16 + log foo
17 +}
18 +----------
19 +{
20 + "logging": {
21 + "logs": {
22 + "access-console": {
23 + "writer": {
24 + "filename": "access-localhost.log",
25 + "output": "file"
26 + },
27 + "encoder": {
28 + "format": "console"
29 + },
30 + "include": [
31 + "http.log.access.foo"
32 + ]
33 + },
34 + "access-json": {
35 + "writer": {
36 + "filename": "access-localhost.json",
37 + "output": "file"
38 + },
39 + "encoder": {
40 + "format": "json"
41 + },
42 + "include": [
43 + "http.log.access.foo"
44 + ]
45 + },
46 + "default": {
47 + "exclude": [
48 + "http.log.access.foo"
49 + ]
50 + }
51 + }
52 + },
53 + "apps": {
54 + "http": {
55 + "servers": {
56 + "srv0": {
57 + "listen": [
58 + ":8881"
59 + ],
60 + "routes": [
61 + {
62 + "match": [
63 + {
64 + "host": [
65 + "localhost"
66 + ]
67 + }
68 + ],
69 + "terminal": true
70 + }
71 + ],
72 + "automatic_https": {
73 + "skip": [
74 + "localhost"
75 + ]
76 + },
77 + "logs": {
78 + "logger_names": {
79 + "localhost:8881": "foo"
80 + }
81 + }
82 + }
83 + }
84 + }
85 + }
86 +}
\ No newline at end of file
caddytest/integration/caddyfile_adapt/log_override_name_multiaccess_debug.txt new
+91
@@ -0,0 +1,91 @@
1 +{
2 + debug
3 +
4 + log access-console {
5 + include http.log.access.foo
6 + output file access-localhost.log
7 + format console
8 + }
9 +
10 + log access-json {
11 + include http.log.access.foo
12 + output file access-localhost.json
13 + format json
14 + }
15 +}
16 +
17 +http://localhost:8881 {
18 + log foo
19 +}
20 +----------
21 +{
22 + "logging": {
23 + "logs": {
24 + "access-console": {
25 + "writer": {
26 + "filename": "access-localhost.log",
27 + "output": "file"
28 + },
29 + "encoder": {
30 + "format": "console"
31 + },
32 + "level": "DEBUG",
33 + "include": [
34 + "http.log.access.foo"
35 + ]
36 + },
37 + "access-json": {
38 + "writer": {
39 + "filename": "access-localhost.json",
40 + "output": "file"
41 + },
42 + "encoder": {
43 + "format": "json"
44 + },
45 + "level": "DEBUG",
46 + "include": [
47 + "http.log.access.foo"
48 + ]
49 + },
50 + "default": {
51 + "level": "DEBUG",
52 + "exclude": [
53 + "http.log.access.foo"
54 + ]
55 + }
56 + }
57 + },
58 + "apps": {
59 + "http": {
60 + "servers": {
61 + "srv0": {
62 + "listen": [
63 + ":8881"
64 + ],
65 + "routes": [
66 + {
67 + "match": [
68 + {
69 + "host": [
70 + "localhost"
71 + ]
72 + }
73 + ],
74 + "terminal": true
75 + }
76 + ],
77 + "automatic_https": {
78 + "skip": [
79 + "localhost"
80 + ]
81 + },
82 + "logs": {
83 + "logger_names": {
84 + "localhost:8881": "foo"
85 + }
86 + }
87 + }
88 + }
89 + }
90 + }
91 +}
\ No newline at end of file