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