From 5d97522d18da39cd3f9dd309774a5ad2c51f4c51 Mon Sep 17 00:00:00 2001 From: Matt Holt Date: Tue, 25 Feb 2020 22:00:33 -0700 Subject: v2: 'log' directive for Caddyfile, and debug mode (#3052) * httpcaddyfile: Begin implementing log directive, and debug mode For now, debug mode just sets the log level for all logs to DEBUG (unless a level is specified explicitly). * httpcaddyfile: Finish 'log' directive Also rename StringEncoder -> SingleFieldEncoder * Fix minor bug in replacer (when vals are empty) --- caddyconfig/httpcaddyfile/builtins.go | 110 ++++++++++++++++++++++++++++++++++ caddyconfig/httpcaddyfile/httptype.go | 67 +++++++++++++++++++++ 2 files changed, 177 insertions(+) (limited to 'caddyconfig') diff --git a/caddyconfig/httpcaddyfile/builtins.go b/caddyconfig/httpcaddyfile/builtins.go index 8cfca18..43e184e 100644 --- a/caddyconfig/httpcaddyfile/builtins.go +++ b/caddyconfig/httpcaddyfile/builtins.go @@ -24,8 +24,10 @@ import ( "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddytls" + "go.uber.org/zap/zapcore" ) func init() { @@ -37,6 +39,7 @@ func init() { RegisterHandlerDirective("route", parseRoute) RegisterHandlerDirective("handle", parseSegmentAsSubroute) RegisterDirective("handle_errors", parseHandleErrors) + RegisterDirective("log", parseLog) } // parseBind parses the bind directive. Syntax: @@ -426,9 +429,116 @@ func parseHandleErrors(h Helper) ([]ConfigValue, error) { }, nil } +// parseLog parses the log directive. Syntax: +// +// log { +// output ... +// format ... +// level +// } +// +func parseLog(h Helper) ([]ConfigValue, error) { + var configValues []ConfigValue + for h.Next() { + cl := new(caddy.CustomLog) + + for h.NextBlock(0) { + switch h.Val() { + case "output": + if !h.NextArg() { + return nil, h.ArgErr() + } + moduleName := h.Val() + + // can't use the usual caddyfile.Unmarshaler flow with the + // standard writers because they are in the caddy package + // (because they are the default) and implementing that + // interface there would unfortunately create circular import + var wo caddy.WriterOpener + switch moduleName { + case "stdout": + wo = caddy.StdoutWriter{} + case "stderr": + wo = caddy.StderrWriter{} + case "discard": + wo = caddy.DiscardWriter{} + default: + mod, err := caddy.GetModule("caddy.logging.writers." + moduleName) + if err != nil { + return nil, h.Errf("getting log writer module named '%s': %v", moduleName, err) + } + unm, ok := mod.New().(caddyfile.Unmarshaler) + if !ok { + return nil, h.Errf("log writer module '%s' is not a Caddyfile unmarshaler", mod) + } + err = unm.UnmarshalCaddyfile(h.NewFromNextSegment()) + if err != nil { + return nil, err + } + wo, ok = unm.(caddy.WriterOpener) + if !ok { + return nil, h.Errf("module %s is not a WriterOpener", mod) + } + } + cl.WriterRaw = caddyconfig.JSONModuleObject(wo, "output", moduleName, h.warnings) + + case "format": + if !h.NextArg() { + return nil, h.ArgErr() + } + moduleName := h.Val() + mod, err := caddy.GetModule("caddy.logging.encoders." + moduleName) + if err != nil { + return nil, h.Errf("getting log encoder module named '%s': %v", moduleName, err) + } + unm, ok := mod.New().(caddyfile.Unmarshaler) + if !ok { + return nil, h.Errf("log encoder module '%s' is not a Caddyfile unmarshaler", mod) + } + err = unm.UnmarshalCaddyfile(h.NewFromNextSegment()) + if err != nil { + return nil, err + } + enc, ok := unm.(zapcore.Encoder) + if !ok { + return nil, h.Errf("module %s is not a zapcore.Encoder", mod) + } + cl.EncoderRaw = caddyconfig.JSONModuleObject(enc, "format", moduleName, h.warnings) + + case "level": + if !h.NextArg() { + return nil, h.ArgErr() + } + cl.Level = h.Val() + if h.NextArg() { + return nil, h.ArgErr() + } + + default: + return nil, h.Errf("unrecognized subdirective: %s", h.Val()) + } + } + + var val namedCustomLog + if !reflect.DeepEqual(cl, new(caddy.CustomLog)) { + cl.Include = []string{"http.log.access"} + val.name = fmt.Sprintf("log%d", logCounter) + val.log = cl + logCounter++ + } + configValues = append(configValues, ConfigValue{ + Class: "custom_log", + Value: val, + }) + } + return configValues, nil +} + // tlsCertTags maps certificate filenames to their tag. // This is used to remember which tag is used for each // certificate files, since we need to avoid loading // the same certificate files more than once, overwriting // previous tags var tlsCertTags = make(map[string]string) + +var logCounter int diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go index d4c2bb6..e3fcdd2 100644 --- a/caddyconfig/httpcaddyfile/httptype.go +++ b/caddyconfig/httpcaddyfile/httptype.go @@ -282,6 +282,35 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock, } } + // extract any custom logs, and enforce configured levels + var customLogs []namedCustomLog + var hasDefaultLog bool + for _, sb := range 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) + } + } + if !hasDefaultLog { + // if the default log was not customized, ensure we + // configure it with any applicable options + if _, ok := options["debug"]; ok { + customLogs = append(customLogs, namedCustomLog{ + name: "default", + log: &caddy.CustomLog{Level: "DEBUG"}, + }) + } + } + // annnd the top-level config, then we're done! cfg := &caddy.Config{AppsRaw: make(caddy.ModuleMap)} if !reflect.DeepEqual(httpApp, caddyhttp.App{}) { @@ -299,6 +328,18 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock, if adminConfig, ok := options["admin"].(string); ok && adminConfig != "" { cfg.Admin = &caddy.AdminConfig{Listen: adminConfig} } + if len(customLogs) > 0 { + if cfg.Logging == nil { + cfg.Logging = &caddy.Logging{ + Logs: make(map[string]*caddy.CustomLog), + } + } + for _, ncl := range customLogs { + if ncl.name != "" { + cfg.Logging.Logs[ncl.name] = ncl.log + } + } + } return cfg, warnings, nil } @@ -335,6 +376,8 @@ func (ServerType) evaluateGlobalOptionsBlock(serverBlocks []serverBlock, options val, err = parseOptEmail(disp) case "admin": val, err = parseOptAdmin(disp) + case "debug": + options["debug"] = true default: return nil, fmt.Errorf("unrecognized parameter name: %s", dir) } @@ -506,6 +549,25 @@ func (st *ServerType) serversFromPairings( srv.Errors.Routes = appendSubrouteToRouteList(srv.Errors.Routes, sr, matcherSetsEnc, p, warnings) } } + + // add log associations + for _, cval := range sblock.pile["custom_log"] { + ncl := cval.Value.(namedCustomLog) + if srv.Logs == nil { + srv.Logs = &caddyhttp.ServerLogConfig{ + LoggerNames: make(map[string]string), + } + } + hosts, err := st.hostsFromServerBlockKeys(sblock.block) + if err != nil { + return nil, err + } + for _, h := range hosts { + if ncl.name != "" { + srv.Logs.LoggerNames[h] = ncl.name + } + } + } } // a catch-all TLS conn policy is necessary to ensure TLS can @@ -940,6 +1002,11 @@ type matcherSetAndTokens struct { tokens []caddyfile.Token } +type namedCustomLog struct { + name string + log *caddy.CustomLog +} + // sbAddrAssocation is a mapping from a list of // addresses to a list of server blocks that are // served on those addresses. -- cgit v1.2.3