From d85cc2ec103de72658c55ba74197337c99bd1f74 Mon Sep 17 00:00:00 2001 From: Omar Ramadan Date: Mon, 10 Jun 2024 08:03:24 -0700 Subject: [PATCH] logging: Customizable zap cores (#6381) --- caddyconfig/httpcaddyfile/builtins.go | 17 ++++++++++ caddyconfig/httpcaddyfile/builtins_test.go | 6 ++-- logging.go | 12 ++++++++ modules/logging/cores.go | 36 ++++++++++++++++++++++ 4 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 modules/logging/cores.go diff --git a/caddyconfig/httpcaddyfile/builtins.go b/caddyconfig/httpcaddyfile/builtins.go index 35a08ef2782..e1e95e00a37 100644 --- a/caddyconfig/httpcaddyfile/builtins.go +++ b/caddyconfig/httpcaddyfile/builtins.go @@ -849,6 +849,7 @@ func parseInvoke(h Helper) (caddyhttp.MiddlewareHandler, error) { // log { // hostnames // output ... +// core ... // format ... // level // } @@ -960,6 +961,22 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue } cl.WriterRaw = caddyconfig.JSONModuleObject(wo, "output", moduleName, h.warnings) + case "core": + if !h.NextArg() { + return nil, h.ArgErr() + } + moduleName := h.Val() + moduleID := "caddy.logging.cores." + moduleName + unm, err := caddyfile.UnmarshalModule(h.Dispenser, moduleID) + if err != nil { + return nil, err + } + core, ok := unm.(zapcore.Core) + if !ok { + return nil, h.Errf("module %s (%T) is not a zapcore.Core", moduleID, unm) + } + cl.CoreRaw = caddyconfig.JSONModuleObject(core, "module", moduleName, h.warnings) + case "format": if !h.NextArg() { return nil, h.ArgErr() diff --git a/caddyconfig/httpcaddyfile/builtins_test.go b/caddyconfig/httpcaddyfile/builtins_test.go index 70f347dd9dc..cf746348487 100644 --- a/caddyconfig/httpcaddyfile/builtins_test.go +++ b/caddyconfig/httpcaddyfile/builtins_test.go @@ -25,11 +25,12 @@ func TestLogDirectiveSyntax(t *testing.T) { { input: `:8080 { log { + core mock output file foo.log } } `, - 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"}}}}}}`, + output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"writer":{"filename":"foo.log","output":"file"},"core":{"module":"mock"},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`, expectError: false, }, { @@ -53,11 +54,12 @@ func TestLogDirectiveSyntax(t *testing.T) { { input: `:8080 { log name-override { + core mock output file foo.log } } `, - 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"}}}}}}`, + output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.name-override"]},"name-override":{"writer":{"filename":"foo.log","output":"file"},"core":{"module":"mock"},"include":["http.log.access.name-override"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"name-override"}}}}}}`, expectError: false, }, } { diff --git a/logging.go b/logging.go index d3e7bf32b56..ca10beeeddc 100644 --- a/logging.go +++ b/logging.go @@ -292,6 +292,10 @@ type BaseLog struct { // The encoder is how the log entries are formatted or encoded. EncoderRaw json.RawMessage `json:"encoder,omitempty" caddy:"namespace=caddy.logging.encoders inline_key=format"` + // Tees entries through a zap.Core module which can extract + // log entry metadata and fields for further processing. + CoreRaw json.RawMessage `json:"core,omitempty" caddy:"namespace=caddy.logging.cores inline_key=module"` + // Level is the minimum level to emit, and is inclusive. // Possible levels: DEBUG, INFO, WARN, ERROR, PANIC, and FATAL Level string `json:"level,omitempty"` @@ -366,6 +370,14 @@ func (cl *BaseLog) provisionCommon(ctx Context, logging *Logging) error { cl.encoder = newDefaultProductionLogEncoder(cl.writerOpener) } cl.buildCore() + if cl.CoreRaw != nil { + mod, err := ctx.LoadModule(cl, "CoreRaw") + if err != nil { + return fmt.Errorf("loading log core module: %v", err) + } + core := mod.(zapcore.Core) + cl.core = zapcore.NewTee(cl.core, core) + } return nil } diff --git a/modules/logging/cores.go b/modules/logging/cores.go new file mode 100644 index 00000000000..49aa7640ce3 --- /dev/null +++ b/modules/logging/cores.go @@ -0,0 +1,36 @@ +package logging + +import ( + "go.uber.org/zap/zapcore" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" +) + +func init() { + caddy.RegisterModule(MockCore{}) +} + +// MockCore is a no-op module, purely for testing +type MockCore struct { + zapcore.Core `json:"-"` +} + +// CaddyModule returns the Caddy module information. +func (MockCore) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "caddy.logging.cores.mock", + New: func() caddy.Module { return new(MockCore) }, + } +} + +func (lec *MockCore) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + return nil +} + +// Interface guards +var ( + _ zapcore.Core = (*MockCore)(nil) + _ caddy.Module = (*MockCore)(nil) + _ caddyfile.Unmarshaler = (*MockCore)(nil) +)