Skip to content

Commit 4cbab42

Browse files
committedNov 23, 2022
support "structured error"
A "structured error" is an error which implements Error and MarshalLog. When encountering such an error, it gets logged normally and then another "errDetails" field gets added with the result of MarshalLog. The extra details are logged as if they had been passed as a value to zapr. Beware that there is no protection against recursion here: if MarshalLog returns the original value, infinite recursion occurs and the program gets killed. A simple guard against this (not expanding error again while formatting an error) is too simplistic and would prevent nice rendering of a wrapped error that might get returned by MarshalLog.
1 parent 4feefdb commit 4cbab42

File tree

1 file changed

+62
-8
lines changed

1 file changed

+62
-8
lines changed
 

Diff for: ‎zapr.go

+62-8
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ type zapLogger struct {
8181
// Logger.Error calls.
8282
errorKey string
8383

84+
// errorKeyDetailsSuffix gets appended to the field name
85+
// when logging additional details obtained via MarshalLog.
86+
errorKeyDetailsSuffix string
87+
8488
// allowZapFields enables logging of strongly-typed Zap
8589
// fields. It is off by default because it breaks
8690
// implementation agnosticism.
@@ -136,15 +140,15 @@ func (zl *zapLogger) handleFields(lvl int, args []interface{}, additional ...zap
136140
continue
137141
}
138142
if zl.panicMessages {
139-
zl.l.WithOptions(zap.AddCallerSkip(1)).DPanic("strongly-typed Zap Field passed to logr", zapIt("zap field", args[i]))
143+
zl.l.WithOptions(zap.AddCallerSkip(1)).DPanic("strongly-typed Zap Field passed to logr", zl.zapIt("zap field", args[i]))
140144
}
141145
break
142146
}
143147

144148
// make sure this isn't a mismatched key
145149
if i == len(args)-1 {
146150
if zl.panicMessages {
147-
zl.l.WithOptions(zap.AddCallerSkip(1)).DPanic("odd number of arguments passed as key-value pairs for logging", zapIt("ignored key", args[i]))
151+
zl.l.WithOptions(zap.AddCallerSkip(1)).DPanic("odd number of arguments passed as key-value pairs for logging", zl.zapIt("ignored key", args[i]))
148152
}
149153
break
150154
}
@@ -156,35 +160,63 @@ func (zl *zapLogger) handleFields(lvl int, args []interface{}, additional ...zap
156160
if !isString {
157161
// if the key isn't a string, DPanic and stop logging
158162
if zl.panicMessages {
159-
zl.l.WithOptions(zap.AddCallerSkip(1)).DPanic("non-string key argument passed to logging, ignoring all later arguments", zapIt("invalid key", key))
163+
zl.l.WithOptions(zap.AddCallerSkip(1)).DPanic("non-string key argument passed to logging, ignoring all later arguments", zl.zapIt("invalid key", key))
160164
}
161165
break
162166
}
163167

164-
fields = append(fields, zapIt(keyStr, val))
168+
fields = append(fields, zl.zapIt(keyStr, val))
165169
i += 2
166170
}
167171

168172
return append(fields, additional...)
169173
}
170174

171-
func zapIt(field string, val interface{}) zap.Field {
175+
func (zl *zapLogger) zapIt(field string, val interface{}) zap.Field {
176+
if err, ok := val.(error); ok {
177+
return zl.zapError(field, err)
178+
}
179+
172180
// Handle types that implement logr.Marshaler: log the replacement
173181
// object instead of the original one.
174182
if marshaler, ok := val.(logr.Marshaler); ok {
175183
field, val = invokeMarshaler(field, marshaler)
176184
}
177-
if keysAndValues, ok := val.(logr.KeysAndValues); ok {
185+
switch val := val.(type) {
186+
case logr.KeysAndValues:
178187
return zap.Object(field, zapcore.ObjectMarshalerFunc(func(encoder zapcore.ObjectEncoder) error {
179-
for _, keyAndValue := range keysAndValues {
188+
for _, keyAndValue := range val {
180189
encoder.AddReflected(keyAndValue.Key, keyAndValue.Value)
181190
}
182191
return nil
183192
}))
184193
}
194+
185195
return zap.Any(field, val)
186196
}
187197

198+
func (zl *zapLogger) zapError(field string, err error) zap.Field {
199+
if err == nil {
200+
return zap.Skip()
201+
}
202+
return zap.Inline(zapcore.ObjectMarshalerFunc(func(encoder zapcore.ObjectEncoder) (retErr error) {
203+
// Always log as a normal error first.
204+
zap.NamedError(field, err).AddTo(encoder)
205+
206+
// Extra details are optional, but might be available if the error also
207+
// implements MarshalLog.
208+
if logMarshaler, ok := err.(logr.Marshaler); ok {
209+
func() {
210+
if r := recover(); r != nil {
211+
retErr = fmt.Errorf("PANIC=%v", r)
212+
}
213+
zl.zapIt(field+zl.errorKeyDetailsSuffix, logMarshaler.MarshalLog()).AddTo(encoder)
214+
}()
215+
}
216+
return
217+
}))
218+
}
219+
188220
func invokeMarshaler(field string, m logr.Marshaler) (f string, ret interface{}) {
189221
defer func() {
190222
if r := recover(); r != nil {
@@ -221,10 +253,22 @@ func (zl *zapLogger) Info(lvl int, msg string, keysAndVals ...interface{}) {
221253

222254
func (zl *zapLogger) Error(err error, msg string, keysAndVals ...interface{}) {
223255
if checkedEntry := zl.l.Check(zap.ErrorLevel, msg); checkedEntry != nil {
224-
checkedEntry.Write(zl.handleFields(noLevel, keysAndVals, zap.NamedError(zl.errorKey, err))...)
256+
checkedEntry.Write(zl.handleFields(noLevel, keysAndVals, zl.zapError(zl.errorKey, err))...)
225257
}
226258
}
227259

260+
// errorToString converts an error to a string,
261+
// handling panics if they occur.
262+
func errorToString(err error) (ret string) {
263+
defer func() {
264+
if err := recover(); err != nil {
265+
ret = fmt.Sprintf("<panic: %s>", err)
266+
}
267+
}()
268+
ret = err.Error()
269+
return
270+
}
271+
228272
func (zl *zapLogger) WithValues(keysAndValues ...interface{}) logr.LogSink {
229273
newLogger := *zl
230274
newLogger.l = zl.l.With(zl.handleFields(noLevel, keysAndValues)...)
@@ -269,6 +313,7 @@ func NewLoggerWithOptions(l *zap.Logger, opts ...Option) logr.Logger {
269313
l: log,
270314
}
271315
zl.errorKey = "error"
316+
zl.errorKeyDetailsSuffix = "Details"
272317
zl.panicMessages = true
273318
for _, option := range opts {
274319
option(zl)
@@ -298,6 +343,15 @@ func ErrorKey(key string) Option {
298343
}
299344
}
300345

346+
// ErrorKeyDetailsSuffix replaces the default "Details" suffix that gets
347+
// appended to the field name for an error when logging the error details
348+
// obtained through MarshalLog.
349+
func ErrorKeyDetailsSuffix(key string) Option {
350+
return func(zl *zapLogger) {
351+
zl.errorKeyDetailsSuffix = key
352+
}
353+
}
354+
301355
// AllowZapFields controls whether strongly-typed Zap fields may
302356
// be passed instead of a key/value pair. This is disabled by
303357
// default because it breaks implementation agnosticism.

0 commit comments

Comments
 (0)