Skip to content

Commit

Permalink
caddyconfig: add global option for configuring loggers
Browse files Browse the repository at this point in the history
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 #3958

[1] https://caddyserver.com/docs/caddyfile/directives/log
  • Loading branch information
kujenga committed Feb 18, 2021
1 parent 4f27927 commit 89eec90
Show file tree
Hide file tree
Showing 5 changed files with 247 additions and 28 deletions.
90 changes: 75 additions & 15 deletions caddyconfig/httpcaddyfile/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -612,18 +612,54 @@ func parseHandleErrors(h Helper) ([]ConfigValue, error) {

// parseLog parses the log directive. Syntax:
//
// log {
// output <writer_module> ...
// format <encoder_module> ...
// level <level>
// log [name] {
// output <writer_module> ...
// format <encoder_module> ...
// level <level>
// include <namespaces...>
// exclude <namespaces...>
// }
//
// 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)
Expand Down Expand Up @@ -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",
Expand Down
30 changes: 28 additions & 2 deletions caddyconfig/httpcaddyfile/builtins_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import (
func TestLogDirectiveSyntax(t *testing.T) {
for i, tc := range []struct {
input string
output string
expectError bool
}{
{
input: `:8080 {
log
}
`,
output: `{"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{}}}}}}`,
expectError: false,
},
{
Expand All @@ -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
}
}
Expand All @@ -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)
}
}
}

Expand Down
31 changes: 20 additions & 11 deletions caddyconfig/httpcaddyfile/httptype.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
}
Expand Down
32 changes: 32 additions & 0 deletions caddyconfig/httpcaddyfile/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 }
Expand Down Expand Up @@ -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 <writer_module> ...
// format <encoder_module> ...
// level <level>
// include <namespaces...>
// exclude <namespaces...>
// }
//
// 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
}
92 changes: 92 additions & 0 deletions caddyconfig/httpcaddyfile/options_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
}

0 comments on commit 89eec90

Please sign in to comment.