zap error of golang

created at 07-05-2021 views: 17

This article mainly studies the zap error of golang.

error

zap@v1.16.0/error.go

var _errArrayElemPool = sync.Pool{New: func() interface{} {
    return &errArrayElem{}
}}

// Error is shorthand for the common idiom NamedError("error", err).
func Error(err error) Field {
    return NamedError("error", err)
}

// NamedError constructs a field that lazily stores err.Error() under the
// provided key. Errors which also implement fmt.Formatter (like those produced
// by github.com/pkg/errors) will also have their verbose representation stored
// under key+"Verbose". If passed a nil error, the field is a no-op.
//
// For the common case in which the key is simply "error", the Error function
// is shorter and less repetitive.
func NamedError(key string, err error) Field {
    if err == nil {
        return Skip()
    }
    return Field{Key: key, Type: zapcore.ErrorType, Interface: err}
}

type errArray []error

func (errs errArray) MarshalLogArray(arr zapcore.ArrayEncoder) error {
    for i := range errs {
        if errs[i] == nil {
            continue
        }
        // To represent each error as an object with an "error" attribute and
        // potentially an "errorVerbose" attribute, we need to wrap it in a
        // type that implements LogObjectMarshaler. To prevent this from
        // allocating, pool the wrapper type.
        elem := _errArrayElemPool.Get().(*errArrayElem)
        elem.error = errs[i]
        arr.AppendObject(elem)
        elem.error = nil
        _errArrayElemPool.Put(elem)
    }
    return nil
}

type errArrayElem struct {
    error
}

func (e *errArrayElem) MarshalLogObject(enc zapcore.ObjectEncoder) error {
    // Re-use the error field's logic, which supports non-standard error types.
    Error(e.error).AddTo(enc)
    return nil
}

Error method uses NamedError to create an err Field; NamedError creates a fieldType of zapcore.ErrorType; errArray type implements ArrayMarshaler's MarshalLogArray method; errArrayElem implements ObjectMarshaler's MarshalLogObject method; error.go defines _errArrayElemPool, and its pool element type is errArrayElem

AddTo

zap@v1.16.0/zapcore/field.go

func (f Field) AddTo(enc ObjectEncoder) {
    var err error

    switch f.Type {
    case ArrayMarshalerType:
        err = enc.AddArray(f.Key, f.Interface.(ArrayMarshaler))
    case ObjectMarshalerType:
        err = enc.AddObject(f.Key, f.Interface.(ObjectMarshaler))
    case BinaryType:
        enc.AddBinary(f.Key, f.Interface.([]byte))
    case BoolType:
        enc.AddBool(f.Key, f.Integer == 1)
    case ByteStringType:
        enc.AddByteString(f.Key, f.Interface.([]byte))
    case Complex128Type:
        enc.AddComplex128(f.Key, f.Interface.(complex128))
    case Complex64Type:
        enc.AddComplex64(f.Key, f.Interface.(complex64))
    case DurationType:
        enc.AddDuration(f.Key, time.Duration(f.Integer))
    case Float64Type:
        enc.AddFloat64(f.Key, math.Float64frombits(uint64(f.Integer)))
    case Float32Type:
        enc.AddFloat32(f.Key, math.Float32frombits(uint32(f.Integer)))
    case Int64Type:
        enc.AddInt64(f.Key, f.Integer)
    case Int32Type:
        enc.AddInt32(f.Key, int32(f.Integer))
    case Int16Type:
        enc.AddInt16(f.Key, int16(f.Integer))
    case Int8Type:
        enc.AddInt8(f.Key, int8(f.Integer))
    case StringType:
        enc.AddString(f.Key, f.String)
    case TimeType:
        if f.Interface != nil {
            enc.AddTime(f.Key, time.Unix(0, f.Integer).In(f.Interface.(*time.Location)))
        } else {
            // Fall back to UTC if location is nil.
            enc.AddTime(f.Key, time.Unix(0, f.Integer))
        }
    case TimeFullType:
        enc.AddTime(f.Key, f.Interface.(time.Time))
    case Uint64Type:
        enc.AddUint64(f.Key, uint64(f.Integer))
    case Uint32Type:
        enc.AddUint32(f.Key, uint32(f.Integer))
    case Uint16Type:
        enc.AddUint16(f.Key, uint16(f.Integer))
    case Uint8Type:
        enc.AddUint8(f.Key, uint8(f.Integer))
    case UintptrType:
        enc.AddUintptr(f.Key, uintptr(f.Integer))
    case ReflectType:
        err = enc.AddReflected(f.Key, f.Interface)
    case NamespaceType:
        enc.OpenNamespace(f.Key)
    case StringerType:
        err = encodeStringer(f.Key, f.Interface, enc)
    case ErrorType:
        encodeError(f.Key, f.Interface.(error), enc)
    case SkipType:
        break
    default:
        panic(fmt.Sprintf("unknown field type: %v", f))
    }

    if err != nil {
        enc.AddString(fmt.Sprintf("%sError", f.Key), err.Error())
    }
}

encodeError

zap@v1.16.0/zapcore/error.go

func encodeError(key string, err error, enc ObjectEncoder) error {
    basic := err.Error()
    enc.AddString(key, basic)

    switch e := err.(type) {
    case errorGroup:
        return enc.AddArray(key+"Causes", errArray(e.Errors()))
    case fmt.Formatter:
        verbose := fmt.Sprintf("%+v", e)
        if verbose != basic {
            // This is a rich error type, like those produced by
            // github.com/pkg/errors.
            enc.AddString(key+"Verbose", verbose)
        }
    }
    return nil
}

type errorGroup interface {
    // Provides read-only access to the underlying list of errors, preferably
    // without causing any allocs.
    Errors() []error
}

type errArray []error

func (errs errArray) MarshalLogArray(arr ArrayEncoder) error {
    for i := range errs {
        if errs[i] == nil {
            continue
        }

        el := newErrArrayElem(errs[i])
        arr.AppendObject(el)
        el.Free()
    }
    return nil
}

The encodeError method judges err.(type), if it is errorGroup, execute enc.AddArray, if it is fmt.Formatter and verbose is not basic, execute enc.AddString(key+"Verbose", verbose)

Stack

zap@v1.16.0/field.go

func Stack(key string) Field {
    return StackSkip(key, 1) // skip Stack
}

func StackSkip(key string, skip int) Field {
    // Returning the stacktrace as a string costs an allocation, but saves us
    // from expanding the zapcore.Field union struct to include a byte slice. Since
    // taking a stacktrace is already so expensive (~10us), the extra allocation
    // is okay.
    return String(key, takeStacktrace(skip+1)) // skip StackSkip
}

Stack executes StackSkip, its skip is 1; StackSkip is used to build a String type stack, while skipping the first few frames, here use takeStacktrace to get the required stack

takeStacktrace

zap@v1.16.0/stacktrace.go

func takeStacktrace(skip int) string {
    buffer := bufferpool.Get()
    defer buffer.Free()
    programCounters := _stacktracePool.Get().(*programCounters)
    defer _stacktracePool.Put(programCounters)

    var numFrames int
    for {
        // Skip the call to runtime.Callers and takeStacktrace so that the
        // program counters start at the caller of takeStacktrace.
        numFrames = runtime.Callers(skip+2, programCounters.pcs)
        if numFrames < len(programCounters.pcs) {
            break
        }
        // Don't put the too-short counter slice back into the pool; this lets
        // the pool adjust if we consistently take deep stacktraces.
        programCounters = newProgramCounters(len(programCounters.pcs) * 2)
    }

    i := 0
    frames := runtime.CallersFrames(programCounters.pcs[:numFrames])

    // Note: On the last iteration, frames.Next() returns false, with a valid
    // frame, but we ignore this frame. The last frame is a a runtime frame which
    // adds noise, since it's only either runtime.main or runtime.goexit.
    for frame, more := frames.Next(); more; frame, more = frames.Next() {
        if i != 0 {
            buffer.AppendByte('\n')
        }
        i++
        buffer.AppendString(frame.Function)
        buffer.AppendByte('\n')
        buffer.AppendByte('\t')
        buffer.AppendString(frame.File)
        buffer.AppendByte(':')
        buffer.AppendInt(int64(frame.Line))
    }

    return buffer.String()
}

The takeStacktrace method obtains frames through runtime.Callers, and then traverses the frames and splices them into strings.

Instance

func errorStacktraceDemo() {
    logger, err := zap.NewDevelopment()
    defer logger.Sync()
    if err != nil {
        panic(err)
    }
    logger.Info("errorField", zap.Error(errors.New("demo err")))

    fmt.Println(zap.Stack("default stack").String)
    fmt.Println("------")
    fmt.Println(zap.StackSkip("skip 2", 2).String)
    logger.Info("stacktrace default", zap.Stack("default stack"))
    logger.Info("stacktrace skip 2", zap.StackSkip("skip 2", 2))
}

Output

2020-12-23T22:19:06.150+0800    INFO    zap/zap_demo.go:29      errorField      {"error": "demo err"}
main.errorStacktraceDemo
        /zap/zap_demo.go:31
main.main
        /zap/zap_demo.go:20
runtime.main
        /usr/local/go/src/runtime/proc.go:204
------
runtime.main
        /usr/local/go/src/runtime/proc.go:204
2020-12-23T22:19:06.150+0800    INFO    zap/zap_demo.go:34      stacktrace default      {"default stack": "main.errorStacktraceDemo\n\t/zap/zap_demo.go:34\nmain.main\n\t/zap/zap_demo.go:20\nruntime.main\n\t/usr/local/go/src/runtime/proc.go:204"}
2020-12-23T22:19:06.150+0800    INFO    zap/zap_demo.go:35      stacktrace skip 2       {"skip 2": "runtime.main\n\t/usr/local/go/src/runtime/proc.go:204"}

summary

Zap provides Error and Stack methods to create ErrorType type errors and StringType stacktrace; ErrorType type Field uses the encodeError method; takeStacktrace method obtains frames through runtime.Callers, and then traverses the frames and splices them into strings.

created at:07-05-2021
edited at: 07-05-2021: