Skip to content

Commit

Permalink
logging: Automatic wrap default for filter encoder (#5980)
Browse files Browse the repository at this point in the history
Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
  • Loading branch information
francislavoie and dunglas committed Jan 25, 2024
1 parent f5344f8 commit b9c40e7
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 27 deletions.
52 changes: 52 additions & 0 deletions caddytest/integration/caddyfile_adapt/log_filter_no_wrap.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
:80

log {
output stdout
format filter {
fields {
request>headers>Server delete
}
}
}
----------
{
"logging": {
"logs": {
"default": {
"exclude": [
"http.log.access.log0"
]
},
"log0": {
"writer": {
"output": "stdout"
},
"encoder": {
"fields": {
"request\u003eheaders\u003eServer": {
"filter": "delete"
}
},
"format": "filter"
},
"include": [
"http.log.access.log0"
]
}
}
},
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"logs": {
"default_logger_name": "log0"
}
}
}
}
}
}
48 changes: 34 additions & 14 deletions logging.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,17 @@ type WriterOpener interface {
OpenWriter() (io.WriteCloser, error)
}

// IsWriterStandardStream returns true if the input is a
// writer-opener to a standard stream (stdout, stderr).
func IsWriterStandardStream(wo WriterOpener) bool {
switch wo.(type) {
case StdoutWriter, StderrWriter,
*StdoutWriter, *StderrWriter:
return true
}
return false
}

type writerDestructor struct {
io.WriteCloser
}
Expand Down Expand Up @@ -341,16 +352,18 @@ func (cl *BaseLog) provisionCommon(ctx Context, logging *Logging) error {
return fmt.Errorf("loading log encoder module: %v", err)
}
cl.encoder = mod.(zapcore.Encoder)

// if the encoder module needs the writer to determine
// the correct default to use for a nested encoder, we
// pass it down as a secondary provisioning step
if cfd, ok := mod.(ConfiguresFormatterDefault); ok {
if err := cfd.ConfigureDefaultFormat(cl.writerOpener); err != nil {
return fmt.Errorf("configuring default format for encoder module: %v", err)
}
}
}
if cl.encoder == nil {
// only allow colorized output if this log is going to stdout or stderr
var colorize bool
switch cl.writerOpener.(type) {
case StdoutWriter, StderrWriter,
*StdoutWriter, *StderrWriter:
colorize = true
}
cl.encoder = newDefaultProductionLogEncoder(colorize)
cl.encoder = newDefaultProductionLogEncoder(cl.writerOpener)
}
cl.buildCore()
return nil
Expand Down Expand Up @@ -680,7 +693,7 @@ func newDefaultProductionLog() (*defaultCustomLog, error) {
if err != nil {
return nil, err
}
cl.encoder = newDefaultProductionLogEncoder(true)
cl.encoder = newDefaultProductionLogEncoder(cl.writerOpener)
cl.levelEnabler = zapcore.InfoLevel

cl.buildCore()
Expand All @@ -697,16 +710,14 @@ func newDefaultProductionLog() (*defaultCustomLog, error) {
}, nil
}

func newDefaultProductionLogEncoder(colorize bool) zapcore.Encoder {
func newDefaultProductionLogEncoder(wo WriterOpener) zapcore.Encoder {
encCfg := zap.NewProductionEncoderConfig()
if term.IsTerminal(int(os.Stdout.Fd())) {
if IsWriterStandardStream(wo) && term.IsTerminal(int(os.Stderr.Fd())) {
// if interactive terminal, make output more human-readable by default
encCfg.EncodeTime = func(ts time.Time, encoder zapcore.PrimitiveArrayEncoder) {
encoder.AppendString(ts.UTC().Format("2006/01/02 15:04:05.000"))
}
if colorize {
encCfg.EncodeLevel = zapcore.CapitalColorLevelEncoder
}
encCfg.EncodeLevel = zapcore.CapitalColorLevelEncoder
return zapcore.NewConsoleEncoder(encCfg)
}
return zapcore.NewJSONEncoder(encCfg)
Expand Down Expand Up @@ -753,6 +764,15 @@ var (

var writers = NewUsagePool()

// ConfiguresFormatterDefault is an optional interface that
// encoder modules can implement to configure the default
// format of their encoder. This is useful for encoders
// which nest an encoder, that needs to know the writer
// in order to determine the correct default.
type ConfiguresFormatterDefault interface {
ConfigureDefaultFormat(WriterOpener) error
}

const DefaultLoggerName = "default"

// Interface guards
Expand Down
66 changes: 53 additions & 13 deletions modules/logging/filterencoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ package logging
import (
"encoding/json"
"fmt"
"os"
"time"

"go.uber.org/zap"
"go.uber.org/zap/buffer"
"go.uber.org/zap/zapcore"
"golang.org/x/term"

"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
Expand All @@ -36,8 +38,10 @@ func init() {
// log entries before they are actually encoded by
// an underlying encoder.
type FilterEncoder struct {
// The underlying encoder that actually
// encodes the log entries. Required.
// The underlying encoder that actually encodes the
// log entries. If not specified, defaults to "json",
// unless the output is a terminal, in which case
// it defaults to "console".
WrappedRaw json.RawMessage `json:"wrap,omitempty" caddy:"namespace=caddy.logging.encoders inline_key=format"`

// A map of field names to their filters. Note that this
Expand All @@ -59,6 +63,9 @@ type FilterEncoder struct {

// used to keep keys unique across nested objects
keyPrefix string

wrappedIsDefault bool
ctx caddy.Context
}

// CaddyModule returns the Caddy module information.
Expand All @@ -71,16 +78,25 @@ func (FilterEncoder) CaddyModule() caddy.ModuleInfo {

// Provision sets up the encoder.
func (fe *FilterEncoder) Provision(ctx caddy.Context) error {
if fe.WrappedRaw == nil {
return fmt.Errorf("missing \"wrap\" (must specify an underlying encoder)")
}
fe.ctx = ctx

// set up wrapped encoder (required)
val, err := ctx.LoadModule(fe, "WrappedRaw")
if err != nil {
return fmt.Errorf("loading fallback encoder module: %v", err)
if fe.WrappedRaw == nil {
// if wrap is not specified, default to JSON
fe.wrapped = &JSONEncoder{}
if p, ok := fe.wrapped.(caddy.Provisioner); ok {
if err := p.Provision(ctx); err != nil {
return fmt.Errorf("provisioning fallback encoder module: %v", err)
}
}
fe.wrappedIsDefault = true
} else {
// set up wrapped encoder
val, err := ctx.LoadModule(fe, "WrappedRaw")
if err != nil {
return fmt.Errorf("loading fallback encoder module: %v", err)
}
fe.wrapped = val.(zapcore.Encoder)
}
fe.wrapped = val.(zapcore.Encoder)

// set up each field filter
if fe.Fields == nil {
Expand All @@ -97,6 +113,29 @@ func (fe *FilterEncoder) Provision(ctx caddy.Context) error {
return nil
}

// ConfigureDefaultFormat will set the default format to "console"
// if the writer is a terminal. If already configured as a filter
// encoder, it passes through the writer so a deeply nested filter
// encoder can configure its own default format.
func (fe *FilterEncoder) ConfigureDefaultFormat(wo caddy.WriterOpener) error {
if !fe.wrappedIsDefault {
if cfd, ok := fe.wrapped.(caddy.ConfiguresFormatterDefault); ok {
return cfd.ConfigureDefaultFormat(wo)
}
return nil
}

if caddy.IsWriterStandardStream(wo) && term.IsTerminal(int(os.Stderr.Fd())) {
fe.wrapped = &ConsoleEncoder{}
if p, ok := fe.wrapped.(caddy.Provisioner); ok {
if err := p.Provision(fe.ctx); err != nil {
return fmt.Errorf("provisioning fallback encoder module: %v", err)
}
}
}
return nil
}

// UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax:
//
// filter {
Expand Down Expand Up @@ -390,7 +429,8 @@ func (mom logObjectMarshalerWrapper) MarshalLogObject(_ zapcore.ObjectEncoder) e

// Interface guards
var (
_ zapcore.Encoder = (*FilterEncoder)(nil)
_ zapcore.ObjectMarshaler = (*logObjectMarshalerWrapper)(nil)
_ caddyfile.Unmarshaler = (*FilterEncoder)(nil)
_ zapcore.Encoder = (*FilterEncoder)(nil)
_ zapcore.ObjectMarshaler = (*logObjectMarshalerWrapper)(nil)
_ caddyfile.Unmarshaler = (*FilterEncoder)(nil)
_ caddy.ConfiguresFormatterDefault = (*FilterEncoder)(nil)
)

0 comments on commit b9c40e7

Please sign in to comment.