How do not lose context with wrapping error with zerolog? How to avoid polluting logs?

Viewed 976

Zerolog's API is designed to provide both a great developer experience and stunning performance. Its unique chaining API allows zerolog to write JSON (or CBOR) log events by avoiding allocations and reflection.

However it works fine when you log just in place. If you want to log somewhere above in stack you can lose moment to log context (struct in pretty format). I tried not log in place and produce json and put as part of error formatting. It can cause problem such as handle struct with nested json.RawMessage fields. Such problem can be easily overcome with zerolog Interface function

func doSmthAgain() error {
    payload := Details{ID: "1", Message: []byte("message")}
    json, err := structToJsonString(payload)
    if err != nil {
        return errors.Wrap(err, "structToJsonString")
    }

    // i want to log json
    // I can log with zerolog right here
    // logger.Err(err).Interface("json", json).Msg("doSmthAgain")
    
    // if i wrap it is possible only use json in formatting so I lose power of zerolog
    return errors.Wrap(fmt.Errorf("example %s", json), "doSmthAgain")
}

// first it does reflection
// then we compose map
// then we marshal it to json and cast to string
func structToJsonString(strct interface{}) (string, error) {
    s := structs.New(strct)
    m := make(map[string]string)
    for _, f := range s.Fields() {
        var v string
        switch f.Value().(type) {
        case json.RawMessage:
            raw, _ := f.Value().(json.RawMessage) // it is already json.RawMessage, don't to check ok
            v = string(raw)
        case string:
            str, _ := f.Value().(string)
            v = string(str)
        default:
            // do smth
        }
        m[f.Name()] = v
    }
    json, err := json.Marshal(m)
    if err != nil {
        return "", errors.Wrap(err, "cannot marshal map")
    }
    return string(json), nil
}

If I log in place with zerolog and return error I log it actually twice because somewere above on stack I still have to handle error I return. So it ends with polluting logs.

func main() {
    if err := doSmth(); err != nil {
        // if log error before, here I log it second time
        logger.Err(err).Msg("Oops!")
    } else {
        logger.Info().Msg("Phew! Everything is ok")
    }

}

func doSmth() error {
    if err := doSmthAgain(); err != nil {
        return errors.Wrap(err, "doSmth")
    }
    return nil
}

func doSmthAgain() error {
    payload := Details{ID: "1", Message: []byte("message")}
    json, err := structToJsonString(payload)
    if err != nil {
        return errors.Wrap(err, "structToJsonString")
    }
    
    // assume I logged here with zerolog
    logger.Err(err).Interface("json", json).Msg("doSmthAgain")
    
    // here it is possible only use json in formatting
    return errors.Wrap(fmt.Errorf("example %s", json), "doSmthAgain")
}

Are there some best practices? Probably I use zerolog in a wrong way?

Playground

0 Answers
Related