Skip to content

Commit

Permalink
Allow configurable stacktrace encoding
Browse files Browse the repository at this point in the history
This PR adds a `StacktraceEncoder` type to the `EncoderConfig` struct

Users can override the `EncodeStacktrace` field to configure how stacktraces are outputed
This PR aims to resolve uber-go#514 by mirroring the behavior of the `EncodeCaller` field

The `EncodeStacktrace` field has been inserted as a required field, and has been added to `NewDevelopmentConfig` and `NewProductionConfig`
as sane defaults however, it is currently a required field and can cause a panic if a user is manually building their config without these helpers.
  • Loading branch information
arwineap committed Oct 9, 2023
1 parent 87577d8 commit 456073a
Show file tree
Hide file tree
Showing 6 changed files with 402 additions and 318 deletions.
50 changes: 26 additions & 24 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,18 +123,19 @@ type Config struct {
// cfg.EncodeTime = zapcore.ISO8601TimeEncoder
func NewProductionEncoderConfig() zapcore.EncoderConfig {
return zapcore.EncoderConfig{
TimeKey: "ts",
LevelKey: "level",
NameKey: "logger",
CallerKey: "caller",
FunctionKey: zapcore.OmitKey,
MessageKey: "msg",
StacktraceKey: "stacktrace",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.LowercaseLevelEncoder,
EncodeTime: zapcore.EpochTimeEncoder,
EncodeDuration: zapcore.SecondsDurationEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
TimeKey: "ts",
LevelKey: "level",
NameKey: "logger",
CallerKey: "caller",
FunctionKey: zapcore.OmitKey,
MessageKey: "msg",
StacktraceKey: "stacktrace",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.LowercaseLevelEncoder,
EncodeTime: zapcore.EpochTimeEncoder,
EncodeDuration: zapcore.SecondsDurationEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
EncodeStacktrace: zapcore.FullStacktraceEncoder,
}
}

Expand Down Expand Up @@ -200,18 +201,19 @@ func NewProductionConfig() Config {
func NewDevelopmentEncoderConfig() zapcore.EncoderConfig {
return zapcore.EncoderConfig{
// Keys can be anything except the empty string.
TimeKey: "T",
LevelKey: "L",
NameKey: "N",
CallerKey: "C",
FunctionKey: zapcore.OmitKey,
MessageKey: "M",
StacktraceKey: "S",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.CapitalLevelEncoder,
EncodeTime: zapcore.ISO8601TimeEncoder,
EncodeDuration: zapcore.StringDurationEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
TimeKey: "T",
LevelKey: "L",
NameKey: "N",
CallerKey: "C",
FunctionKey: zapcore.OmitKey,
MessageKey: "M",
StacktraceKey: "S",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.CapitalLevelEncoder,
EncodeTime: zapcore.ISO8601TimeEncoder,
EncodeDuration: zapcore.StringDurationEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
EncodeStacktrace: zapcore.FullStacktraceEncoder,
}
}

Expand Down
18 changes: 16 additions & 2 deletions zapcore/console_encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ func (c consoleEncoder) EncodeEntry(ent Entry, fields []Field) (*buffer.Buffer,
arr.AppendString(ent.Caller.Function)
}
}

for i := range arr.elems {
if i > 0 {
line.AppendString(c.ConsoleSeparator)
Expand All @@ -119,10 +120,23 @@ func (c consoleEncoder) EncodeEntry(ent Entry, fields []Field) (*buffer.Buffer,
c.writeContext(line, fields)

// If there's no stacktrace key, honor that; this allows users to force
// single-line output.
// single-line output by avoiding printing the stacktrace.
if ent.Stack != "" && c.StacktraceKey != "" {
line.AppendByte('\n')
line.AppendString(ent.Stack)

if c.EncodeStacktrace != nil {
arr = getSliceEncoder()
c.EncodeStacktrace(ent.Stack, arr)
for i := range arr.elems {
if i > 0 {
line.AppendString(c.ConsoleSeparator)
}
fmt.Fprint(line, arr.elems[i])
}
putSliceEncoder(arr)
} else {
line.AppendString(ent.Stack)
}
}

line.AppendString(c.LineEnding)
Expand Down
21 changes: 17 additions & 4 deletions zapcore/encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,18 @@ func (e *DurationEncoder) UnmarshalText(text []byte) error {
return nil
}

// A StacktraceEncoder serializes a stacktrace
//
// This function must make exactly one call
// to a PrimitiveArrayEncoder's Append* method.
type StacktraceEncoder func(string, PrimitiveArrayEncoder)

// FullStacktraceEncoder passes down the full stacktrace as a string to the enc
func FullStacktraceEncoder(stacktrace string, enc PrimitiveArrayEncoder) {
// TODO: consider using a byte-oriented API to save an allocation.
enc.AppendString(stacktrace)
}

// A CallerEncoder serializes an EntryCaller to a primitive type.
//
// This function must make exactly one call
Expand Down Expand Up @@ -343,10 +355,11 @@ type EncoderConfig struct {
// Configure the primitive representations of common complex types. For
// example, some users may want all time.Times serialized as floating-point
// seconds since epoch, while others may prefer ISO8601 strings.
EncodeLevel LevelEncoder `json:"levelEncoder" yaml:"levelEncoder"`
EncodeTime TimeEncoder `json:"timeEncoder" yaml:"timeEncoder"`
EncodeDuration DurationEncoder `json:"durationEncoder" yaml:"durationEncoder"`
EncodeCaller CallerEncoder `json:"callerEncoder" yaml:"callerEncoder"`
EncodeLevel LevelEncoder `json:"levelEncoder" yaml:"levelEncoder"`
EncodeTime TimeEncoder `json:"timeEncoder" yaml:"timeEncoder"`
EncodeDuration DurationEncoder `json:"durationEncoder" yaml:"durationEncoder"`
EncodeCaller CallerEncoder `json:"callerEncoder" yaml:"callerEncoder"`
EncodeStacktrace StacktraceEncoder `json:"stacktraceEncoder" yaml:"stacktraceEncoder"`
// Unlike the other primitive type encoders, EncodeName is optional. The
// zero value falls back to FullNameEncoder.
EncodeName NameEncoder `json:"nameEncoder" yaml:"nameEncoder"`
Expand Down

0 comments on commit 456073a

Please sign in to comment.