Repository: rs/zerolog
Pull request #185 (2019-10-01 20:52:54)

Stringer event method
Call to log.Str("url", url.String()) can lead to unnecessary allocations while executing String().
So introduce new log.Stringer("url", url) method, that call to String() only if logging enabled.


Patch:
diff --git a/README.md b/README.md
index 718681a..e1403fe 100644
--- a/README.md
+++ b/README.md
@@ -31,7 +31,7 @@ Find out [who uses zerolog](https://github.com/rs/zerolog/wiki/Who-uses-zerolog)
 
 ## Installation
 
-```go
+```bash
 go get -u github.com/rs/zerolog/log
 ```
 
@@ -341,7 +341,7 @@ If your writer might be slow or not thread-safe and you need your log producers
 wr := diode.NewWriter(os.Stdout, 1000, 10*time.Millisecond, func(missed int) {
 		fmt.Printf("Logger Dropped %d messages", missed)
 	})
-log := zerolog.New(w)
+log := zerolog.New(wr)
 log.Print("test")
 ```
 
@@ -435,11 +435,11 @@ c := alice.New()
 c = c.Append(hlog.NewHandler(log))
 
 // Install some provided extra handler to set some request's context fields.
-// Thanks to those handler, all our logs will come with some pre-populated fields.
+// Thanks to that handler, all our logs will come with some prepopulated fields.
 c = c.Append(hlog.AccessHandler(func(r *http.Request, status, size int, duration time.Duration) {
     hlog.FromRequest(r).Info().
         Str("method", r.Method).
-        Str("url", r.URL.String()).
+        Stringer("url", r.URL).
         Int("status", status).
         Int("size", size).
         Dur("duration", duration).
@@ -469,12 +469,31 @@ if err := http.ListenAndServe(":8080", nil); err != nil {
 }
 ```
 
+## Multiple Log Output
+`zerolog.MultiLevelWriter` may be used to send the log message to multiple outputs. 
+In this example, we send the log message to both `os.Stdout` and the in-built ConsoleWriter.
+```go
+func main() {
+	consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout}
+
+	multi := zerolog.MultiLevelWriter(consoleWriter, os.Stdout)
+
+	logger := zerolog.New(multi).With().Timestamp().Logger()
+
+	logger.Info().Msg("Hello World!")
+}
+
+// Output (Line 1: Console; Line 2: Stdout)
+// 12:36PM INF Hello World!
+// {"level":"info","time":"2019-11-07T12:36:38+03:00","message":"Hello World!"}
+``` 
+
 ## Global Settings
 
 Some settings can be changed and will by applied to all loggers:
 
 * `log.Logger`: You can set this value to customize the global logger (the one used by package level methods).
-* `zerolog.SetGlobalLevel`: Can raise the minimum level of all loggers. Set this to `zerolog.Disabled` to disable logging altogether (quiet mode).
+* `zerolog.SetGlobalLevel`: Can raise the minimum level of all loggers. Call this with `zerolog.Disabled` to disable logging altogether (quiet mode).
 * `zerolog.DisableSampling`: If argument is `true`, all sampled loggers will stop sampling and issue 100% of their log events.
 * `zerolog.TimestampFieldName`: Can be set to customize `Timestamp` field name.
 * `zerolog.LevelFieldName`: Can be set to customize level field name.
@@ -497,13 +516,17 @@ Some settings can be changed and will by applied to all loggers:
 
 ### Advanced Fields
 
-* `Err`: Takes an `error` and render it as a string using the `zerolog.ErrorFieldName` field name.
-* `Timestamp`: Insert a timestamp field with `zerolog.TimestampFieldName` field name and formatted using `zerolog.TimeFieldFormat`.
-* `Time`: Adds a field with the time formated with the `zerolog.TimeFieldFormat`.
-* `Dur`: Adds a field with a `time.Duration`.
+* `Err`: Takes an `error` and renders it as a string using the `zerolog.ErrorFieldName` field name.
+* `Timestamp`: Inserts a timestamp field with `zerolog.TimestampFieldName` field name, formatted using `zerolog.TimeFieldFormat`.
+* `Time`: Adds a field with time formatted with `zerolog.TimeFieldFormat`.
+* `Dur`: Adds a field with `time.Duration`.
 * `Dict`: Adds a sub-key/value as a field of the event.
+* `RawJSON`: Adds a field with an already encoded JSON (`[]byte`)
+* `Hex`: Adds a field with value formatted as a hexadecimal string (`[]byte`)
 * `Interface`: Uses reflection to marshal the type.
 
+Most fields are also available in the slice format (`Strs` for `[]string`, `Errs` for `[]error` etc.)
+
 ## Binary Encoding
 
 In addition to the default JSON encoding, `zerolog` can produce binary logs using [CBOR](http://cbor.io) encoding. The choice of encoding can be decided at compile time using the build tag `binary_log` as follows:
diff --git a/array.go b/array.go
index d88c497..0f7f53e 100644
--- a/array.go
+++ b/array.go
@@ -93,8 +93,7 @@ func (a *Array) RawJSON(val []byte) *Array {
 
 // Err serializes and appends the err to the array.
 func (a *Array) Err(err error) *Array {
-	marshaled := ErrorMarshalFunc(err)
-	switch m := marshaled.(type) {
+	switch m := ErrorMarshalFunc(err).(type) {
 	case LogObjectMarshaler:
 		e := newEvent(nil, 0)
 		e.buf = e.buf[:0]
@@ -102,7 +101,11 @@ func (a *Array) Err(err error) *Array {
 		a.buf = append(enc.AppendArrayDelim(a.buf), e.buf...)
 		putEvent(e)
 	case error:
-		a.buf = enc.AppendString(enc.AppendArrayDelim(a.buf), m.Error())
+		if m == nil || isNilValue(m) {
+			a.buf = enc.AppendNil(enc.AppendArrayDelim(a.buf))
+		} else {
+			a.buf = enc.AppendString(enc.AppendArrayDelim(a.buf), m.Error())
+		}
 	case string:
 		a.buf = enc.AppendString(enc.AppendArrayDelim(a.buf), m)
 	default:
diff --git a/console.go b/console.go
index 75f6e45..54f7994 100644
--- a/console.go
+++ b/console.go
@@ -300,9 +300,13 @@ func consoleDefaultFormatTimestamp(timeFormat string, noColor bool) Formatter {
 				t = tt.String()
 			} else {
 				var sec, nsec int64 = i, 0
-				if TimeFieldFormat == TimeFormatUnixMs {
+				switch TimeFieldFormat {
+				case TimeFormatUnixMs:
 					nsec = int64(time.Duration(i) * time.Millisecond)
 					sec = 0
+				case TimeFormatUnixMicro:
+					nsec = int64(time.Duration(i) * time.Microsecond)
+					sec = 0
 				}
 				ts := time.Unix(sec, nsec).UTC()
 				t = ts.Format(timeFormat)
diff --git a/context.go b/context.go
index 1815a08..27f0d9a 100644
--- a/context.go
+++ b/context.go
@@ -104,14 +104,17 @@ func (c Context) RawJSON(key string, b []byte) Context {
 
 // AnErr adds the field key with serialized err to the logger context.
 func (c Context) AnErr(key string, err error) Context {
-	marshaled := ErrorMarshalFunc(err)
-	switch m := marshaled.(type) {
+	switch m := ErrorMarshalFunc(err).(type) {
 	case nil:
 		return c
 	case LogObjectMarshaler:
 		return c.Object(key, m)
 	case error:
-		return c.Str(key, m.Error())
+		if m == nil || isNilValue(m) {
+			return c
+		} else {
+			return c.Str(key, m.Error())
+		}
 	case string:
 		return c.Str(key, m)
 	default:
@@ -124,12 +127,15 @@ func (c Context) AnErr(key string, err error) Context {
 func (c Context) Errs(key string, errs []error) Context {
 	arr := Arr()
 	for _, err := range errs {
-		marshaled := ErrorMarshalFunc(err)
-		switch m := marshaled.(type) {
+		switch m := ErrorMarshalFunc(err).(type) {
 		case LogObjectMarshaler:
 			arr = arr.Object(m)
 		case error:
-			arr = arr.Str(m.Error())
+			if m == nil || isNilValue(m) {
+				arr = arr.Interface(nil)
+			} else {
+				arr = arr.Str(m.Error())
+			}
 		case string:
 			arr = arr.Str(m)
 		default:
diff --git a/diode/internal/diodes/many_to_one.go b/diode/internal/diodes/many_to_one.go
index 0f562f7..e94a788 100644
--- a/diode/internal/diodes/many_to_one.go
+++ b/diode/internal/diodes/many_to_one.go
@@ -9,9 +9,9 @@ import (
 // ManyToOne diode is optimal for many writers (go-routines B-n) and a single
 // reader (go-routine A). It is not thread safe for multiple readers.
 type ManyToOne struct {
-	buffer     []unsafe.Pointer
 	writeIndex uint64
 	readIndex  uint64
+	buffer     []unsafe.Pointer
 	alerter    Alerter
 }
 
diff --git a/diode/internal/diodes/one_to_one.go b/diode/internal/diodes/one_to_one.go
index aaf66d1..dd85ab0 100644
--- a/diode/internal/diodes/one_to_one.go
+++ b/diode/internal/diodes/one_to_one.go
@@ -31,9 +31,9 @@ type bucket struct {
 // OneToOne diode is meant to be used by a single reader and a single writer.
 // It is not thread safe if used otherwise.
 type OneToOne struct {
-	buffer     []unsafe.Pointer
 	writeIndex uint64
 	readIndex  uint64
+	buffer     []unsafe.Pointer
 	alerter    Alerter
 }
 
diff --git a/event.go b/event.go
index 1402835..f1829be 100644
--- a/event.go
+++ b/event.go
@@ -61,6 +61,7 @@ func newEvent(w LevelWriter, level Level) *Event {
 	e.buf = enc.AppendBeginMarker(e.buf)
 	e.w = w
 	e.level = level
+	e.stack = false
 	return e
 }
 
@@ -235,6 +236,21 @@ func (e *Event) Strs(key string, vals []string) *Event {
 	return e
 }
 
+// Stringer adds the field key with val.String() (or null if val is nil) to the *Event context.
+func (e *Event) Stringer(key string, val fmt.Stringer) *Event {
+	if e == nil {
+		return e
+	}
+
+	if val != nil {
+		e.buf = enc.AppendString(enc.AppendKey(e.buf, key), val.String())
+		return e
+	}
+
+	e.buf = enc.AppendInterface(enc.AppendKey(e.buf, key), nil)
+	return e
+}
+
 // Bytes adds the field key with val as a string to the *Event context.
 //
 // Runes outside of normal ASCII ranges will be hex-encoded in the resulting
@@ -280,7 +296,11 @@ func (e *Event) AnErr(key string, err error) *Event {
 	case LogObjectMarshaler:
 		return e.Object(key, m)
 	case error:
-		return e.Str(key, m.Error())
+		if m == nil || isNilValue(m) {
+			return e
+		} else {
+			return e.Str(key, m.Error())
+		}
 	case string:
 		return e.Str(key, m)
 	default:
@@ -313,7 +333,6 @@ func (e *Event) Errs(key string, errs []error) *Event {
 
 // Err adds the field "error" with serialized err to the *Event context.
 // If err is nil, no field is added.
-// To customize the key name, change zerolog.ErrorFieldName.
 //
 // To customize the key name, change zerolog.ErrorFieldName.
 //
@@ -330,7 +349,9 @@ func (e *Event) Err(err error) *Event {
 		case LogObjectMarshaler:
 			e.Object(ErrorStackFieldName, m)
 		case error:
-			e.Str(ErrorStackFieldName, m.Error())
+			if m != nil && !isNilValue(m) {
+				e.Str(ErrorStackFieldName, m.Error())
+			}
 		case string:
 			e.Str(ErrorStackFieldName, m)
 		default:
diff --git a/fields.go b/fields.go
index 6b62ecc..cf3c3e9 100644
--- a/fields.go
+++ b/fields.go
@@ -4,8 +4,13 @@ import (
 	"net"
 	"sort"
 	"time"
+	"unsafe"
 )
 
+func isNilValue(i interface{}) bool {
+	return (*[2]uintptr)(unsafe.Pointer(&i))[1] == 0
+}
+
 func appendFields(dst []byte, fields map[string]interface{}) []byte {
 	keys := make([]string, 0, len(fields))
 	for key := range fields {
@@ -29,8 +34,7 @@ func appendFields(dst []byte, fields map[string]interface{}) []byte {
 		case []byte:
 			dst = enc.AppendBytes(dst, val)
 		case error:
-			marshaled := ErrorMarshalFunc(val)
-			switch m := marshaled.(type) {
+			switch m := ErrorMarshalFunc(val).(type) {
 			case LogObjectMarshaler:
 				e := newEvent(nil, 0)
 				e.buf = e.buf[:0]
@@ -38,7 +42,11 @@ func appendFields(dst []byte, fields map[string]interface{}) []byte {
 				dst = append(dst, e.buf...)
 				putEvent(e)
 			case error:
-				dst = enc.AppendString(dst, m.Error())
+				if m == nil || isNilValue(m) {
+					dst = enc.AppendNil(dst)
+				} else {
+					dst = enc.AppendString(dst, m.Error())
+				}
 			case string:
 				dst = enc.AppendString(dst, m)
 			default:
@@ -47,8 +55,7 @@ func appendFields(dst []byte, fields map[string]interface{}) []byte {
 		case []error:
 			dst = enc.AppendArrayStart(dst)
 			for i, err := range val {
-				marshaled := ErrorMarshalFunc(err)
-				switch m := marshaled.(type) {
+				switch m := ErrorMarshalFunc(err).(type) {
 				case LogObjectMarshaler:
 					e := newEvent(nil, 0)
 					e.buf = e.buf[:0]
@@ -56,7 +63,11 @@ func appendFields(dst []byte, fields map[string]interface{}) []byte {
 					dst = append(dst, e.buf...)
 					putEvent(e)
 				case error:
-					dst = enc.AppendString(dst, m.Error())
+					if m == nil || isNilValue(m) {
+						dst = enc.AppendNil(dst)
+					} else {
+						dst = enc.AppendString(dst, m.Error())
+					}
 				case string:
 					dst = enc.AppendString(dst, m)
 				default:
diff --git a/go.mod b/go.mod
index 8c42ba8..340ed40 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,5 @@ require (
 	github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e
 	github.com/pkg/errors v0.8.1
 	github.com/rs/xid v1.2.1
-	github.com/zenazn/goji v0.9.0
 	golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74
 )
diff --git a/go.sum b/go.sum
index b14fd2b..13b9b2c 100644
--- a/go.sum
+++ b/go.sum
@@ -4,8 +4,6 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc=
 github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
-github.com/zenazn/goji v0.9.0 h1:RSQQAbXGArQ0dIDEq+PI6WqN6if+5KHu6x2Cx/GXLTQ=
-github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
diff --git a/hlog/hlog.go b/hlog/hlog.go
index 37f4336..8748b25 100644
--- a/hlog/hlog.go
+++ b/hlog/hlog.go
@@ -9,8 +9,8 @@ import (
 
 	"github.com/rs/xid"
 	"github.com/rs/zerolog"
+	"github.com/rs/zerolog/hlog/internal/mutil"
 	"github.com/rs/zerolog/log"
-	"github.com/zenazn/goji/web/mutil"
 )
 
 // FromRequest gets the logger in the request's context.
diff --git a/hlog/internal/mutil/LICENSE b/hlog/internal/mutil/LICENSE
new file mode 100644
index 0000000..446aba0
--- /dev/null
+++ b/hlog/internal/mutil/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2014, 2015, 2016 Carl Jackson (carl@avtok.com)
+
+MIT License
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/hlog/internal/mutil/mutil.go b/hlog/internal/mutil/mutil.go
new file mode 100644
index 0000000..80d6ac6
--- /dev/null
+++ b/hlog/internal/mutil/mutil.go
@@ -0,0 +1,6 @@
+// Package mutil contains various functions that are helpful when writing http
+// middleware.
+//
+// It has been vendored from Goji v1.0, with the exception of the code for Go 1.8:
+// https://github.com/zenazn/goji/
+package mutil
diff --git a/hlog/internal/mutil/writer_proxy.go b/hlog/internal/mutil/writer_proxy.go
new file mode 100644
index 0000000..598a86f
--- /dev/null
+++ b/hlog/internal/mutil/writer_proxy.go
@@ -0,0 +1,149 @@
+package mutil
+
+import (
+	"bufio"
+	"io"
+	"net"
+	"net/http"
+)
+
+// WriterProxy is a proxy around an http.ResponseWriter that allows you to hook
+// into various parts of the response process.
+type WriterProxy interface {
+	http.ResponseWriter
+	// Status returns the HTTP status of the request, or 0 if one has not
+	// yet been sent.
+	Status() int
+	// BytesWritten returns the total number of bytes sent to the client.
+	BytesWritten() int
+	// Tee causes the response body to be written to the given io.Writer in
+	// addition to proxying the writes through. Only one io.Writer can be
+	// tee'd to at once: setting a second one will overwrite the first.
+	// Writes will be sent to the proxy before being written to this
+	// io.Writer. It is illegal for the tee'd writer to be modified
+	// concurrently with writes.
+	Tee(io.Writer)
+	// Unwrap returns the original proxied target.
+	Unwrap() http.ResponseWriter
+}
+
+// WrapWriter wraps an http.ResponseWriter, returning a proxy that allows you to
+// hook into various parts of the response process.
+func WrapWriter(w http.ResponseWriter) WriterProxy {
+	_, cn := w.(http.CloseNotifier)
+	_, fl := w.(http.Flusher)
+	_, hj := w.(http.Hijacker)
+	_, rf := w.(io.ReaderFrom)
+
+	bw := basicWriter{ResponseWriter: w}
+	if cn && fl && hj && rf {
+		return &fancyWriter{bw}
+	}
+	if fl {
+		return &flushWriter{bw}
+	}
+	return &bw
+}
+
+// basicWriter wraps a http.ResponseWriter that implements the minimal
+// http.ResponseWriter interface.
+type basicWriter struct {
+	http.ResponseWriter
+	wroteHeader bool
+	code        int
+	bytes       int
+	tee         io.Writer
+}
+
+func (b *basicWriter) WriteHeader(code int) {
+	if !b.wroteHeader {
+		b.code = code
+		b.wroteHeader = true
+		b.ResponseWriter.WriteHeader(code)
+	}
+}
+
+func (b *basicWriter) Write(buf []byte) (int, error) {
+	b.WriteHeader(http.StatusOK)
+	n, err := b.ResponseWriter.Write(buf)
+	if b.tee != nil {
+		_, err2 := b.tee.Write(buf[:n])
+		// Prefer errors generated by the proxied writer.
+		if err == nil {
+			err = err2
+		}
+	}
+	b.bytes += n
+	return n, err
+}
+
+func (b *basicWriter) maybeWriteHeader() {
+	if !b.wroteHeader {
+		b.WriteHeader(http.StatusOK)
+	}
+}
+
+func (b *basicWriter) Status() int {
+	return b.code
+}
+
+func (b *basicWriter) BytesWritten() int {
+	return b.bytes
+}
+
+func (b *basicWriter) Tee(w io.Writer) {
+	b.tee = w
+}
+
+func (b *basicWriter) Unwrap() http.ResponseWriter {
+	return b.ResponseWriter
+}
+
+// fancyWriter is a writer that additionally satisfies http.CloseNotifier,
+// http.Flusher, http.Hijacker, and io.ReaderFrom. It exists for the common case
+// of wrapping the http.ResponseWriter that package http gives you, in order to
+// make the proxied object support the full method set of the proxied object.
+type fancyWriter struct {
+	basicWriter
+}
+
+func (f *fancyWriter) CloseNotify() <-chan bool {
+	cn := f.basicWriter.ResponseWriter.(http.CloseNotifier)
+	return cn.CloseNotify()
+}
+
+func (f *fancyWriter) Flush() {
+	fl := f.basicWriter.ResponseWriter.(http.Flusher)
+	fl.Flush()
+}
+
+func (f *fancyWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
+	hj := f.basicWriter.ResponseWriter.(http.Hijacker)
+	return hj.Hijack()
+}
+
+func (f *fancyWriter) ReadFrom(r io.Reader) (int64, error) {
+	if f.basicWriter.tee != nil {
+		return io.Copy(&f.basicWriter, r)
+	}
+	rf := f.basicWriter.ResponseWriter.(io.ReaderFrom)
+	f.basicWriter.maybeWriteHeader()
+	return rf.ReadFrom(r)
+}
+
+type flushWriter struct {
+	basicWriter
+}
+
+func (f *flushWriter) Flush() {
+	fl := f.basicWriter.ResponseWriter.(http.Flusher)
+	fl.Flush()
+}
+
+var (
+	_ http.CloseNotifier = &fancyWriter{}
+	_ http.Flusher       = &fancyWriter{}
+	_ http.Hijacker      = &fancyWriter{}
+	_ io.ReaderFrom      = &fancyWriter{}
+	_ http.Flusher       = &flushWriter{}
+)
diff --git a/internal/json/base.go b/internal/json/base.go
index d6f8839..62248e7 100644
--- a/internal/json/base.go
+++ b/internal/json/base.go
@@ -4,9 +4,8 @@ type Encoder struct{}
 
 // AppendKey appends a new key to the output JSON.
 func (e Encoder) AppendKey(dst []byte, key string) []byte {
-	if len(dst) > 1 && dst[len(dst)-1] != '{' {
+	if dst[len(dst)-1] != '{' {
 		dst = append(dst, ',')
 	}
-	dst = e.AppendString(dst, key)
-	return append(dst, ':')
-}
\ No newline at end of file
+	return append(e.AppendString(dst, key), ':')
+}
diff --git a/internal/json/types.go b/internal/json/types.go
index bc8bc09..d186242 100644
--- a/internal/json/types.go
+++ b/internal/json/types.go
@@ -379,11 +379,10 @@ func (Encoder) AppendObjectData(dst []byte, o []byte) []byte {
 	//    to separate with existing content OR
 	// 3. existing content has already other fields
 	if o[0] == '{' {
-		if len(dst) == 0 {
-			o = o[1:]
-		} else {
-			o[0] = ','
+		if len(dst) > 1 {
+			dst = append(dst, ',')
 		}
+		o = o[1:]
 	} else if len(dst) > 1 {
 		dst = append(dst, ',')
 	}
diff --git a/journald/journald.go b/journald/journald.go
index 058a68a..630ed7e 100644
--- a/journald/journald.go
+++ b/journald/journald.go
@@ -73,7 +73,9 @@ func (w journalWriter) Write(p []byte) (n int, err error) {
 		err = fmt.Errorf("cannot connect to journalD")
 		return
 	}
+
 	var event map[string]interface{}
+	origPLen := len(p)
 	p = cbor.DecodeIfBinaryToBytes(p)
 	d := json.NewDecoder(bytes.NewReader(p))
 	d.UseNumber()
@@ -114,5 +116,10 @@ func (w journalWriter) Write(p []byte) (n int, err error) {
 	}
 	args["JSON"] = string(p)
 	err = journal.Send(msg, jPrio, args)
+
+	if err == nil {
+		n = origPLen
+	}
+
 	return
 }
diff --git a/log.go b/log.go
index 0af296c..cbf6885 100644
--- a/log.go
+++ b/log.go
@@ -179,7 +179,7 @@ func ParseLevel(levelStr string) (Level, error) {
 
 // A Logger represents an active logging object that generates lines
 // of JSON output to an io.Writer. Each logging operation makes a single
-// call to the Writer's Write method. There is no guaranty on access
+// call to the Writer's Write method. There is no guarantee on access
 // serialization to the Writer. If your Writer is not thread safe,
 // you may consider a sync wrapper.
 type Logger struct {
@@ -195,7 +195,7 @@ type Logger struct {
 // one.
 //
 // Each logging operation makes a single call to the Writer's Write method. There is no
-// guaranty on access serialization to the Writer. If your Writer is not thread safe,
+// guarantee on access serialization to the Writer. If your Writer is not thread safe,
 // you may consider using sync wrapper.
 func New(w io.Writer) Logger {
 	if w == nil {
@@ -234,6 +234,10 @@ func (l Logger) With() Context {
 	l.context = make([]byte, 0, 500)
 	if context != nil {
 		l.context = append(l.context, context...)
+	} else {
+		// This is needed for AppendKey to not check len of input
+		// thus making it inlinable
+		l.context = enc.AppendBeginMarker(l.context)
 	}
 	return Context{l}
 }
@@ -415,7 +419,7 @@ func (l *Logger) newEvent(level Level, done func(string)) *Event {
 	if level != NoLevel {
 		e.Str(LevelFieldName, LevelFieldMarshalFunc(level))
 	}
-	if l.context != nil && len(l.context) > 0 {
+	if l.context != nil && len(l.context) > 1 {
 		e.buf = enc.AppendObjectData(e.buf, l.context)
 	}
 	return e
diff --git a/writer.go b/writer.go
index a58d717..381b0f6 100644
--- a/writer.go
+++ b/writer.go
@@ -26,11 +26,9 @@ type syncWriter struct {
 }
 
 // SyncWriter wraps w so that each call to Write is synchronized with a mutex.
-// This syncer can be the call to writer's Write method is not thread safe.
-// Note that os.File Write operation is using write() syscall which is supposed
-// to be thread-safe on POSIX systems. So there is no need to use this with
-// os.File on such systems as zerolog guaranties to issue a single Write call
-// per log event.
+// This syncer can be used to wrap the call to writer's Write method if it is
+// not thread safe. Note that you do not need this wrapper for os.File Write
+// operations on POSIX and Windows systems as they are already thread-safe.
 func SyncWriter(w io.Writer) io.Writer {
 	if lw, ok := w.(LevelWriter); ok {
 		return &syncWriter{lw: lw}


Test patch:
diff --git a/console_test.go b/console_test.go
index 4567e9e..751cdcd 100644
--- a/console_test.go
+++ b/console_test.go
@@ -165,6 +165,28 @@ func TestConsoleWriter(t *testing.T) {
 		}
 	})
 
+	t.Run("Unix timestamp us input format", func(t *testing.T) {
+		of := zerolog.TimeFieldFormat
+		defer func() {
+			zerolog.TimeFieldFormat = of
+		}()
+		zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMicro
+
+		buf := &bytes.Buffer{}
+		w := zerolog.ConsoleWriter{Out: buf, TimeFormat: time.StampMicro, NoColor: true}
+
+		_, err := w.Write([]byte(`{"time": 1234567891, "level": "debug", "message": "Foobar", "foo": "bar"}`))
+		if err != nil {
+			t.Errorf("Unexpected error when writing output: %s", err)
+		}
+
+		expectedOutput := "Jan  1 00:20:34.567891 DBG Foobar foo=bar\n"
+		actualOutput := buf.String()
+		if actualOutput != expectedOutput {
+			t.Errorf("Unexpected output %q, want: %q", actualOutput, expectedOutput)
+		}
+	})
+
 	t.Run("No message field", func(t *testing.T) {
 		buf := &bytes.Buffer{}
 		w := zerolog.ConsoleWriter{Out: buf, NoColor: true}
diff --git a/event_test.go b/event_test.go
new file mode 100644
index 0000000..879eca1
--- /dev/null
+++ b/event_test.go
@@ -0,0 +1,39 @@
+// +build !binary_log
+
+package zerolog
+
+import (
+	"bytes"
+	"errors"
+	"strings"
+	"testing"
+)
+
+type nilError struct{}
+
+func (nilError) Error() string {
+	return ""
+}
+
+func TestEvent_AnErr(t *testing.T) {
+	tests := []struct {
+		name string
+		err  error
+		want string
+	}{
+		{"nil", nil, `{}`},
+		{"error", errors.New("test"), `{"err":"test"}`},
+		{"nil interface", func() *nilError { return nil }(), `{}`},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			var buf bytes.Buffer
+			e := newEvent(levelWriterAdapter{&buf}, DebugLevel)
+			e.AnErr("err", tt.err)
+			_ = e.write()
+			if got, want := strings.TrimSpace(buf.String()), tt.want; got != want {
+				t.Errorf("Event.AnErr() = %v, want %v", got, want)
+			}
+		})
+	}
+}
diff --git a/internal/cbor/types_test.go b/internal/cbor/types_test.go
index 9c31429..48c2083 100644
--- a/internal/cbor/types_test.go
+++ b/internal/cbor/types_test.go
@@ -88,11 +88,8 @@ var integerTestCases = []struct {
 	{0xFFFF, "\x19\xff\xff"},
 	// Value in 4 bytes.
 	{0x10000, "\x1a\x00\x01\x00\x00"},
-	{0xFFFFFFFE, "\x1a\xff\xff\xff\xfe"},
+	{0x7FFFFFFE, "\x1a\x7f\xff\xff\xfe"},
 	{1000000, "\x1a\x00\x0f\x42\x40"},
-	// Value in 8 bytes.
-	{0xabcd100000000, "\x1b\x00\x0a\xbc\xd1\x00\x00\x00\x00"},
-	{1000000000000, "\x1b\x00\x00\x00\xe8\xd4\xa5\x10\x00"},
 	// Negative number test cases.
 	// Value included in the type.
 	{-1, "\x20"},
@@ -116,11 +113,9 @@ var integerTestCases = []struct {
 	{-1000, "\x39\x03\xe7"},
 	// Value in 4 bytes.
 	{-0x10001, "\x3a\x00\x01\x00\x00"},
-	{-0xFFFFFFFE, "\x3a\xff\xff\xff\xfd"},
+	{-0x7FFFFFFE, "\x3a\x7f\xff\xff\xfd"},
 	{-1000000, "\x3a\x00\x0f\x42\x3f"},
-	// Value in 8 bytes.
-	{-0xabcd100000001, "\x3b\x00\x0a\xbc\xd1\x00\x00\x00\x00"},
-	{-1000000000001, "\x3b\x00\x00\x00\xe8\xd4\xa5\x10\x00"},
+
 }
 
 func TestAppendInt(t *testing.T) {
diff --git a/internal/cbor/types_test_64.go b/internal/cbor/types_test_64.go
new file mode 100644
index 0000000..728b171
--- /dev/null
+++ b/internal/cbor/types_test_64.go
@@ -0,0 +1,35 @@
+// +build !386
+
+package cbor
+
+import (
+	"encoding/hex"
+	"testing"
+)
+
+var enc2 = Encoder{}
+
+var integerTestCases_64bit = []struct {
+	val    int
+	binary string
+}{
+	// Value in 8 bytes.
+	{0xabcd100000000, "\x1b\x00\x0a\xbc\xd1\x00\x00\x00\x00"},
+	{1000000000000, "\x1b\x00\x00\x00\xe8\xd4\xa5\x10\x00"},
+	// Value in 8 bytes.
+	{-0xabcd100000001, "\x3b\x00\x0a\xbc\xd1\x00\x00\x00\x00"},
+	{-1000000000001, "\x3b\x00\x00\x00\xe8\xd4\xa5\x10\x00"},
+
+}
+
+func TestAppendInt_64bit(t *testing.T) {
+	for _, tc := range integerTestCases_64bit {
+		s := enc2.AppendInt([]byte{}, tc.val)
+		got := string(s)
+		if got != tc.binary {
+			t.Errorf("AppendInt(0x%x)=0x%s, want: 0x%s",
+				tc.val, hex.EncodeToString(s),
+				hex.EncodeToString([]byte(tc.binary)))
+		}
+	}
+}
diff --git a/journald/journald_test.go b/journald/journald_test.go
index 7ea40b5..3379fb0 100644
--- a/journald/journald_test.go
+++ b/journald/journald_test.go
@@ -1,9 +1,15 @@
-// +build !windows
+// +build linux
 
 package journald_test
 
-import "github.com/rs/zerolog"
-import "github.com/rs/zerolog/journald"
+import (
+	"bytes"
+	"io"
+	"testing"
+
+	"github.com/rs/zerolog"
+	"github.com/rs/zerolog/journald"
+)
 
 func ExampleNewJournalDWriter() {
 	log := zerolog.New(journald.NewJournalDWriter())
@@ -42,3 +48,39 @@ Thu 2018-04-26 22:30:20.768136 PDT [s=3284d695bde946e4b5017c77a399237f;i=329f0;b
     _PID=27103
     _SOURCE_REALTIME_TIMESTAMP=1524807020768136
 */
+
+func TestWriteReturnsNoOfWrittenBytes(t *testing.T) {
+	input := []byte(`{"level":"info","time":1570912626,"message":"Starting..."}`)
+	wr := journald.NewJournalDWriter()
+	want := len(input)
+	got, err := wr.Write(input)
+
+	if err != nil {
+		t.Errorf("Unexpected error %v", err)
+	}
+
+	if want != got {
+		t.Errorf("Expected %d bytes to be written got %d", want, got)
+	}
+}
+
+func TestMultiWrite(t *testing.T) {
+	var (
+		w1 = new(bytes.Buffer)
+		w2 = new(bytes.Buffer)
+		w3 = journald.NewJournalDWriter()
+	)
+
+	zerolog.ErrorHandler = func(err error) {
+		if err == io.ErrShortWrite {
+			t.Errorf("Unexpected ShortWriteError")
+			t.FailNow()
+		}
+	}
+
+	log := zerolog.New(io.MultiWriter(w1, w2, w3)).With().Logger()
+
+	for i := 0; i < 10; i++ {
+		log.Info().Msg("Tick!")
+	}
+}
diff --git a/log_test.go b/log_test.go
index dee2916..4a4740c 100644
--- a/log_test.go
+++ b/log_test.go
@@ -235,6 +235,8 @@ func TestFields(t *testing.T) {
 	log.Log().
 		Caller().
 		Str("string", "foo").
+		Stringer("stringer", net.IP{127, 0, 0, 1}).
+		Stringer("stringer_nil", nil).
 		Bytes("bytes", []byte("bar")).
 		Hex("hex", []byte{0x12, 0xef}).
 		RawJSON("json", []byte(`{"some":"json"}`)).
@@ -261,7 +263,7 @@ func TestFields(t *testing.T) {
 		Time("time", time.Time{}).
 		TimeDiff("diff", now, now.Add(-10*time.Second)).
 		Msg("")
-	if got, want := decodeIfBinaryToString(out.Bytes()), `{"caller":"`+caller+`","string":"foo","bytes":"bar","hex":"12ef","json":{"some":"json"},"error":"some error","bool":true,"int":1,"int8":2,"int16":3,"int32":4,"int64":5,"uint":6,"uint8":7,"uint16":8,"uint32":9,"uint64":10,"IPv4":"192.168.0.100","IPv6":"2001:db8:85a3::8a2e:370:7334","Mac":"00:14:22:01:23:45","Prefix":"192.168.0.100/24","float32":11.1234,"float64":12.321321321,"dur":1000,"time":"0001-01-01T00:00:00Z","diff":10000}`+"\n"; got != want {
+	if got, want := decodeIfBinaryToString(out.Bytes()), `{"caller":"`+caller+`","string":"foo","stringer":"127.0.0.1","stringer_nil":null,"bytes":"bar","hex":"12ef","json":{"some":"json"},"error":"some error","bool":true,"int":1,"int8":2,"int16":3,"int32":4,"int64":5,"uint":6,"uint8":7,"uint16":8,"uint32":9,"uint64":10,"IPv4":"192.168.0.100","IPv6":"2001:db8:85a3::8a2e:370:7334","Mac":"00:14:22:01:23:45","Prefix":"192.168.0.100/24","float32":11.1234,"float64":12.321321321,"dur":1000,"time":"0001-01-01T00:00:00Z","diff":10000}`+"\n"; got != want {
 		t.Errorf("invalid log output:\ngot:  %v\nwant: %v", got, want)
 	}
 }