Repository: rs/zerolog
Pull request #550 (2023-05-06 16:48:21)

Add fields order
After I did this work I saw PR 170 which is apparently languishing. My bad for not checking first. On the other hand this PR currently has no merge issues and I have time to respond to comments.





rs: Could you please make this change not allocating additional memory on each write if the feature is not used?
madkins23: Good idea. That also cleaned up the code a lot. Not sure what I was thinking when I did all that before. :-(

I stored a `map[string]bool` on the `ConsoleWriter` to speed up subsequent calls. It won't get created until the first call to `orderFields` so it's just a wasted pointer otherwise, and `ConsoleWriter` shouldn't be created a bunch of times unless I'm missing something.

BTW, there's another similar situation with `FieldsExclude` at the beginning of `ConsoleWriter.writeFields` that could be solved the same way.  The current solution iterates over `w.FieldsExclude` for each field which feels like `O(n^2)`. I don't mind writing the PR if you agree @rs.

Both of those last two notes assume that `ConsoleWriter` fields don't get changed over time. If that is not true then I need to remove that tweak on this PR. The `w.FieldsExclude` issue might still benefit from being converted to a `map[string]bool` at the beginning of  `ConsoleWriter.writeFields`.

Patch:
diff --git a/README.md b/README.md
index 5b1ab73..1306a6c 100644
--- a/README.md
+++ b/README.md
@@ -638,10 +638,14 @@ Some settings can be changed and will be applied to all loggers:
 * `zerolog.LevelFieldName`: Can be set to customize level field name.
 * `zerolog.MessageFieldName`: Can be set to customize message field name.
 * `zerolog.ErrorFieldName`: Can be set to customize `Err` field name.
-* `zerolog.TimeFieldFormat`: Can be set to customize `Time` field value formatting. If set with `zerolog.TimeFormatUnix`, `zerolog.TimeFormatUnixMs` or `zerolog.TimeFormatUnixMicro`, times are formated as UNIX timestamp.
+* `zerolog.TimeFieldFormat`: Can be set to customize `Time` field value formatting. If set with `zerolog.TimeFormatUnix`, `zerolog.TimeFormatUnixMs` or `zerolog.TimeFormatUnixMicro`, times are formatted as UNIX timestamp.
 * `zerolog.DurationFieldUnit`: Can be set to customize the unit for time.Duration type fields added by `Dur` (default: `time.Millisecond`).
 * `zerolog.DurationFieldInteger`: If set to `true`, `Dur` fields are formatted as integers instead of floats (default: `false`). 
 * `zerolog.ErrorHandler`: Called whenever zerolog fails to write an event on its output. If not set, an error is printed on the stderr. This handler must be thread safe and non-blocking.
+* `zerolog.FloatingPointPrecision`: If set to a value other than -1, controls the number
+of digits when formatting float numbers in JSON. See
+[strconv.FormatFloat](https://pkg.go.dev/strconv#FormatFloat)
+for more details.
 
 ## Field Types
 
diff --git a/array.go b/array.go
index 99612ee..ba35b28 100644
--- a/array.go
+++ b/array.go
@@ -183,13 +183,13 @@ func (a *Array) Uint64(i uint64) *Array {
 
 // Float32 appends f as a float32 to the array.
 func (a *Array) Float32(f float32) *Array {
-	a.buf = enc.AppendFloat32(enc.AppendArrayDelim(a.buf), f)
+	a.buf = enc.AppendFloat32(enc.AppendArrayDelim(a.buf), f, FloatingPointPrecision)
 	return a
 }
 
 // Float64 appends f as a float64 to the array.
 func (a *Array) Float64(f float64) *Array {
-	a.buf = enc.AppendFloat64(enc.AppendArrayDelim(a.buf), f)
+	a.buf = enc.AppendFloat64(enc.AppendArrayDelim(a.buf), f, FloatingPointPrecision)
 	return a
 }
 
@@ -201,7 +201,7 @@ func (a *Array) Time(t time.Time) *Array {
 
 // Dur appends d to the array.
 func (a *Array) Dur(d time.Duration) *Array {
-	a.buf = enc.AppendDuration(enc.AppendArrayDelim(a.buf), d, DurationFieldUnit, DurationFieldInteger)
+	a.buf = enc.AppendDuration(enc.AppendArrayDelim(a.buf), d, DurationFieldUnit, DurationFieldInteger, FloatingPointPrecision)
 	return a
 }
 
diff --git a/console.go b/console.go
index 10c1a1c..7e65e86 100644
--- a/console.go
+++ b/console.go
@@ -28,6 +28,8 @@ const (
 
 	colorBold     = 1
 	colorDarkGray = 90
+
+	unknownLevel = "???"
 )
 
 var (
@@ -67,6 +69,11 @@ type ConsoleWriter struct {
 	// PartsExclude defines parts to not display in output.
 	PartsExclude []string
 
+	// FieldsOrder defines the order of contextual fields in output.
+	FieldsOrder []string
+
+	fieldIsOrdered map[string]int
+
 	// FieldsExclude defines contextual fields to not display in output.
 	FieldsExclude []string
 
@@ -189,7 +196,12 @@ func (w ConsoleWriter) writeFields(evt map[string]interface{}, buf *bytes.Buffer
 		}
 		fields = append(fields, field)
 	}
-	sort.Strings(fields)
+
+	if len(w.FieldsOrder) > 0 {
+		w.orderFields(fields)
+	} else {
+		sort.Strings(fields)
+	}
 
 	// Write space only if something has already been written to the buffer, and if there are fields.
 	if buf.Len() > 0 && len(fields) > 0 {
@@ -322,6 +334,32 @@ func (w ConsoleWriter) writePart(buf *bytes.Buffer, evt map[string]interface{},
 	}
 }
 
+// orderFields takes an array of field names and an array representing field order
+// and returns an array with any ordered fields at the beginning, in order,
+// and the remaining fields after in their original order.
+func (w ConsoleWriter) orderFields(fields []string) {
+	if w.fieldIsOrdered == nil {
+		w.fieldIsOrdered = make(map[string]int)
+		for i, fieldName := range w.FieldsOrder {
+			w.fieldIsOrdered[fieldName] = i
+		}
+	}
+	sort.Slice(fields, func(i, j int) bool {
+		ii, iOrdered := w.fieldIsOrdered[fields[i]]
+		jj, jOrdered := w.fieldIsOrdered[fields[j]]
+		if iOrdered && jOrdered {
+			return ii < jj
+		}
+		if iOrdered {
+			return true
+		}
+		if jOrdered {
+			return false
+		}
+		return fields[i] < fields[j]
+	})
+}
+
 // needsQuote returns true when the string s should be quoted in output.
 func needsQuote(s string) bool {
 	for i := range s {
@@ -400,25 +438,30 @@ func consoleDefaultFormatTimestamp(timeFormat string, location *time.Location, n
 	}
 }
 
+func stripLevel(ll string) string {
+	if len(ll) == 0 {
+		return unknownLevel
+	}
+	if len(ll) > 3 {
+		ll = ll[:3]
+	}
+	return strings.ToUpper(ll)
+}
+
 func consoleDefaultFormatLevel(noColor bool) Formatter {
 	return func(i interface{}) string {
-		var l string
 		if ll, ok := i.(string); ok {
 			level, _ := ParseLevel(ll)
 			fl, ok := FormattedLevels[level]
 			if ok {
-				l = colorize(fl, LevelColors[level], noColor)
-			} else {
-				l = strings.ToUpper(ll)[0:3]
-			}
-		} else {
-			if i == nil {
-				l = "???"
-			} else {
-				l = strings.ToUpper(fmt.Sprintf("%s", i))[0:3]
+				return colorize(fl, LevelColors[level], noColor)
 			}
+			return stripLevel(ll)
+		}
+		if i == nil {
+			return unknownLevel
 		}
-		return l
+		return stripLevel(fmt.Sprintf("%s", i))
 	}
 }
 
diff --git a/context.go b/context.go
index df352eb..ff9a3ae 100644
--- a/context.go
+++ b/context.go
@@ -325,25 +325,25 @@ func (c Context) Uints64(key string, i []uint64) Context {
 
 // Float32 adds the field key with f as a float32 to the logger context.
 func (c Context) Float32(key string, f float32) Context {
-	c.l.context = enc.AppendFloat32(enc.AppendKey(c.l.context, key), f)
+	c.l.context = enc.AppendFloat32(enc.AppendKey(c.l.context, key), f, FloatingPointPrecision)
 	return c
 }
 
 // Floats32 adds the field key with f as a []float32 to the logger context.
 func (c Context) Floats32(key string, f []float32) Context {
-	c.l.context = enc.AppendFloats32(enc.AppendKey(c.l.context, key), f)
+	c.l.context = enc.AppendFloats32(enc.AppendKey(c.l.context, key), f, FloatingPointPrecision)
 	return c
 }
 
 // Float64 adds the field key with f as a float64 to the logger context.
 func (c Context) Float64(key string, f float64) Context {
-	c.l.context = enc.AppendFloat64(enc.AppendKey(c.l.context, key), f)
+	c.l.context = enc.AppendFloat64(enc.AppendKey(c.l.context, key), f, FloatingPointPrecision)
 	return c
 }
 
 // Floats64 adds the field key with f as a []float64 to the logger context.
 func (c Context) Floats64(key string, f []float64) Context {
-	c.l.context = enc.AppendFloats64(enc.AppendKey(c.l.context, key), f)
+	c.l.context = enc.AppendFloats64(enc.AppendKey(c.l.context, key), f, FloatingPointPrecision)
 	return c
 }
 
@@ -365,13 +365,13 @@ func (c Context) Timestamp() Context {
 	return c
 }
 
-// Time adds the field key with t formated as string using zerolog.TimeFieldFormat.
+// Time adds the field key with t formatted as string using zerolog.TimeFieldFormat.
 func (c Context) Time(key string, t time.Time) Context {
 	c.l.context = enc.AppendTime(enc.AppendKey(c.l.context, key), t, TimeFieldFormat)
 	return c
 }
 
-// Times adds the field key with t formated as string using zerolog.TimeFieldFormat.
+// Times adds the field key with t formatted as string using zerolog.TimeFieldFormat.
 func (c Context) Times(key string, t []time.Time) Context {
 	c.l.context = enc.AppendTimes(enc.AppendKey(c.l.context, key), t, TimeFieldFormat)
 	return c
@@ -379,13 +379,13 @@ func (c Context) Times(key string, t []time.Time) Context {
 
 // Dur adds the fields key with d divided by unit and stored as a float.
 func (c Context) Dur(key string, d time.Duration) Context {
-	c.l.context = enc.AppendDuration(enc.AppendKey(c.l.context, key), d, DurationFieldUnit, DurationFieldInteger)
+	c.l.context = enc.AppendDuration(enc.AppendKey(c.l.context, key), d, DurationFieldUnit, DurationFieldInteger, FloatingPointPrecision)
 	return c
 }
 
 // Durs adds the fields key with d divided by unit and stored as a float.
 func (c Context) Durs(key string, d []time.Duration) Context {
-	c.l.context = enc.AppendDurations(enc.AppendKey(c.l.context, key), d, DurationFieldUnit, DurationFieldInteger)
+	c.l.context = enc.AppendDurations(enc.AppendKey(c.l.context, key), d, DurationFieldUnit, DurationFieldInteger, FloatingPointPrecision)
 	return c
 }
 
@@ -409,6 +409,12 @@ func (c Context) Any(key string, i interface{}) Context {
 	return c.Interface(key, i)
 }
 
+// Reset removes all the context fields.
+func (c Context) Reset() Context {
+	c.l.context = enc.AppendBeginMarker(make([]byte, 0, 500))
+	return c
+}
+
 type callerHook struct {
 	callerSkipFrameCount int
 }
diff --git a/encoder.go b/encoder.go
index 09b24e8..4dbaf38 100644
--- a/encoder.go
+++ b/encoder.go
@@ -13,13 +13,13 @@ type encoder interface {
 	AppendBool(dst []byte, val bool) []byte
 	AppendBools(dst []byte, vals []bool) []byte
 	AppendBytes(dst, s []byte) []byte
-	AppendDuration(dst []byte, d time.Duration, unit time.Duration, useInt bool) []byte
-	AppendDurations(dst []byte, vals []time.Duration, unit time.Duration, useInt bool) []byte
+	AppendDuration(dst []byte, d time.Duration, unit time.Duration, useInt bool, precision int) []byte
+	AppendDurations(dst []byte, vals []time.Duration, unit time.Duration, useInt bool, precision int) []byte
 	AppendEndMarker(dst []byte) []byte
-	AppendFloat32(dst []byte, val float32) []byte
-	AppendFloat64(dst []byte, val float64) []byte
-	AppendFloats32(dst []byte, vals []float32) []byte
-	AppendFloats64(dst []byte, vals []float64) []byte
+	AppendFloat32(dst []byte, val float32, precision int) []byte
+	AppendFloat64(dst []byte, val float64, precision int) []byte
+	AppendFloats32(dst []byte, vals []float32, precision int) []byte
+	AppendFloats64(dst []byte, vals []float64, precision int) []byte
 	AppendHex(dst, s []byte) []byte
 	AppendIPAddr(dst []byte, ip net.IP) []byte
 	AppendIPPrefix(dst []byte, pfx net.IPNet) []byte
diff --git a/event.go b/event.go
index 5c949f8..56de606 100644
--- a/event.go
+++ b/event.go
@@ -644,7 +644,7 @@ func (e *Event) Float32(key string, f float32) *Event {
 	if e == nil {
 		return e
 	}
-	e.buf = enc.AppendFloat32(enc.AppendKey(e.buf, key), f)
+	e.buf = enc.AppendFloat32(enc.AppendKey(e.buf, key), f, FloatingPointPrecision)
 	return e
 }
 
@@ -653,7 +653,7 @@ func (e *Event) Floats32(key string, f []float32) *Event {
 	if e == nil {
 		return e
 	}
-	e.buf = enc.AppendFloats32(enc.AppendKey(e.buf, key), f)
+	e.buf = enc.AppendFloats32(enc.AppendKey(e.buf, key), f, FloatingPointPrecision)
 	return e
 }
 
@@ -662,7 +662,7 @@ func (e *Event) Float64(key string, f float64) *Event {
 	if e == nil {
 		return e
 	}
-	e.buf = enc.AppendFloat64(enc.AppendKey(e.buf, key), f)
+	e.buf = enc.AppendFloat64(enc.AppendKey(e.buf, key), f, FloatingPointPrecision)
 	return e
 }
 
@@ -671,7 +671,7 @@ func (e *Event) Floats64(key string, f []float64) *Event {
 	if e == nil {
 		return e
 	}
-	e.buf = enc.AppendFloats64(enc.AppendKey(e.buf, key), f)
+	e.buf = enc.AppendFloats64(enc.AppendKey(e.buf, key), f, FloatingPointPrecision)
 	return e
 }
 
@@ -713,7 +713,7 @@ func (e *Event) Dur(key string, d time.Duration) *Event {
 	if e == nil {
 		return e
 	}
-	e.buf = enc.AppendDuration(enc.AppendKey(e.buf, key), d, DurationFieldUnit, DurationFieldInteger)
+	e.buf = enc.AppendDuration(enc.AppendKey(e.buf, key), d, DurationFieldUnit, DurationFieldInteger, FloatingPointPrecision)
 	return e
 }
 
@@ -724,7 +724,7 @@ func (e *Event) Durs(key string, d []time.Duration) *Event {
 	if e == nil {
 		return e
 	}
-	e.buf = enc.AppendDurations(enc.AppendKey(e.buf, key), d, DurationFieldUnit, DurationFieldInteger)
+	e.buf = enc.AppendDurations(enc.AppendKey(e.buf, key), d, DurationFieldUnit, DurationFieldInteger, FloatingPointPrecision)
 	return e
 }
 
@@ -739,7 +739,7 @@ func (e *Event) TimeDiff(key string, t time.Time, start time.Time) *Event {
 	if t.After(start) {
 		d = t.Sub(start)
 	}
-	e.buf = enc.AppendDuration(enc.AppendKey(e.buf, key), d, DurationFieldUnit, DurationFieldInteger)
+	e.buf = enc.AppendDuration(enc.AppendKey(e.buf, key), d, DurationFieldUnit, DurationFieldInteger, FloatingPointPrecision)
 	return e
 }
 
diff --git a/fields.go b/fields.go
index 23606dd..99f5271 100644
--- a/fields.go
+++ b/fields.go
@@ -139,13 +139,13 @@ func appendFieldList(dst []byte, kvList []interface{}, stack bool) []byte {
 		case uint64:
 			dst = enc.AppendUint64(dst, val)
 		case float32:
-			dst = enc.AppendFloat32(dst, val)
+			dst = enc.AppendFloat32(dst, val, FloatingPointPrecision)
 		case float64:
-			dst = enc.AppendFloat64(dst, val)
+			dst = enc.AppendFloat64(dst, val, FloatingPointPrecision)
 		case time.Time:
 			dst = enc.AppendTime(dst, val, TimeFieldFormat)
 		case time.Duration:
-			dst = enc.AppendDuration(dst, val, DurationFieldUnit, DurationFieldInteger)
+			dst = enc.AppendDuration(dst, val, DurationFieldUnit, DurationFieldInteger, FloatingPointPrecision)
 		case *string:
 			if val != nil {
 				dst = enc.AppendString(dst, *val)
@@ -220,13 +220,13 @@ func appendFieldList(dst []byte, kvList []interface{}, stack bool) []byte {
 			}
 		case *float32:
 			if val != nil {
-				dst = enc.AppendFloat32(dst, *val)
+				dst = enc.AppendFloat32(dst, *val, FloatingPointPrecision)
 			} else {
 				dst = enc.AppendNil(dst)
 			}
 		case *float64:
 			if val != nil {
-				dst = enc.AppendFloat64(dst, *val)
+				dst = enc.AppendFloat64(dst, *val, FloatingPointPrecision)
 			} else {
 				dst = enc.AppendNil(dst)
 			}
@@ -238,7 +238,7 @@ func appendFieldList(dst []byte, kvList []interface{}, stack bool) []byte {
 			}
 		case *time.Duration:
 			if val != nil {
-				dst = enc.AppendDuration(dst, *val, DurationFieldUnit, DurationFieldInteger)
+				dst = enc.AppendDuration(dst, *val, DurationFieldUnit, DurationFieldInteger, FloatingPointPrecision)
 			} else {
 				dst = enc.AppendNil(dst)
 			}
@@ -267,13 +267,13 @@ func appendFieldList(dst []byte, kvList []interface{}, stack bool) []byte {
 		case []uint64:
 			dst = enc.AppendUints64(dst, val)
 		case []float32:
-			dst = enc.AppendFloats32(dst, val)
+			dst = enc.AppendFloats32(dst, val, FloatingPointPrecision)
 		case []float64:
-			dst = enc.AppendFloats64(dst, val)
+			dst = enc.AppendFloats64(dst, val, FloatingPointPrecision)
 		case []time.Time:
 			dst = enc.AppendTimes(dst, val, TimeFieldFormat)
 		case []time.Duration:
-			dst = enc.AppendDurations(dst, val, DurationFieldUnit, DurationFieldInteger)
+			dst = enc.AppendDurations(dst, val, DurationFieldUnit, DurationFieldInteger, FloatingPointPrecision)
 		case nil:
 			dst = enc.AppendNil(dst)
 		case net.IP:
diff --git a/globals.go b/globals.go
index ac1d9c4..9a9be71 100644
--- a/globals.go
+++ b/globals.go
@@ -151,6 +151,11 @@ var (
 	// TriggerLevelWriterBufferReuseLimit is a limit in bytes that a buffer is dropped
 	// from the TriggerLevelWriter buffer pool if the buffer grows above the limit.
 	TriggerLevelWriterBufferReuseLimit = 64 * 1024
+
+	// FloatingPointPrecision, if set to a value other than -1, controls the number
+	// of digits when formatting float numbers in JSON. See strconv.FormatFloat for
+	// more details.
+	FloatingPointPrecision = -1
 )
 
 var (
diff --git a/internal/cbor/decode_stream.go b/internal/cbor/decode_stream.go
index 616bed6..5633e66 100644
--- a/internal/cbor/decode_stream.go
+++ b/internal/cbor/decode_stream.go
@@ -95,7 +95,7 @@ func decodeFloat(src *bufio.Reader) (float64, int) {
 
 	switch minor {
 	case additionalTypeFloat16:
-		panic(fmt.Errorf("float16 is not suppported in decodeFloat"))
+		panic(fmt.Errorf("float16 is not supported in decodeFloat"))
 
 	case additionalTypeFloat32:
 		pb := readNBytes(src, 4)
diff --git a/internal/cbor/time.go b/internal/cbor/time.go
index d81fb12..7c0ecce 100644
--- a/internal/cbor/time.go
+++ b/internal/cbor/time.go
@@ -29,7 +29,7 @@ func (e Encoder) appendFloatTimestamp(dst []byte, t time.Time) []byte {
 	nanos := t.Nanosecond()
 	var val float64
 	val = float64(secs)*1.0 + float64(nanos)*1e-9
-	return e.AppendFloat64(dst, val)
+	return e.AppendFloat64(dst, val, -1)
 }
 
 // AppendTime encodes and adds a timestamp to the dst byte array.
@@ -64,17 +64,17 @@ func (e Encoder) AppendTimes(dst []byte, vals []time.Time, unused string) []byte
 // AppendDuration encodes and adds a duration to the dst byte array.
 // useInt field indicates whether to store the duration as seconds (integer) or
 // as seconds+nanoseconds (float).
-func (e Encoder) AppendDuration(dst []byte, d time.Duration, unit time.Duration, useInt bool) []byte {
+func (e Encoder) AppendDuration(dst []byte, d time.Duration, unit time.Duration, useInt bool, unused int) []byte {
 	if useInt {
 		return e.AppendInt64(dst, int64(d/unit))
 	}
-	return e.AppendFloat64(dst, float64(d)/float64(unit))
+	return e.AppendFloat64(dst, float64(d)/float64(unit), unused)
 }
 
 // AppendDurations encodes and adds an array of durations to the dst byte array.
 // useInt field indicates whether to store the duration as seconds (integer) or
 // as seconds+nanoseconds (float).
-func (e Encoder) AppendDurations(dst []byte, vals []time.Duration, unit time.Duration, useInt bool) []byte {
+func (e Encoder) AppendDurations(dst []byte, vals []time.Duration, unit time.Duration, useInt bool, unused int) []byte {
 	major := majorTypeArray
 	l := len(vals)
 	if l == 0 {
@@ -87,7 +87,7 @@ func (e Encoder) AppendDurations(dst []byte, vals []time.Duration, unit time.Dur
 		dst = appendCborTypePrefix(dst, major, uint64(l))
 	}
 	for _, d := range vals {
-		dst = e.AppendDuration(dst, d, unit, useInt)
+		dst = e.AppendDuration(dst, d, unit, useInt, unused)
 	}
 	return dst
 }
diff --git a/internal/cbor/types.go b/internal/cbor/types.go
index 6f53832..454d68b 100644
--- a/internal/cbor/types.go
+++ b/internal/cbor/types.go
@@ -352,7 +352,7 @@ func (e Encoder) AppendUints64(dst []byte, vals []uint64) []byte {
 }
 
 // AppendFloat32 encodes and inserts a single precision float value into the dst byte array.
-func (Encoder) AppendFloat32(dst []byte, val float32) []byte {
+func (Encoder) AppendFloat32(dst []byte, val float32, unused int) []byte {
 	switch {
 	case math.IsNaN(float64(val)):
 		return append(dst, "\xfa\x7f\xc0\x00\x00"...)
@@ -372,7 +372,7 @@ func (Encoder) AppendFloat32(dst []byte, val float32) []byte {
 }
 
 // AppendFloats32 encodes and inserts an array of single precision float value into the dst byte array.
-func (e Encoder) AppendFloats32(dst []byte, vals []float32) []byte {
+func (e Encoder) AppendFloats32(dst []byte, vals []float32, unused int) []byte {
 	major := majorTypeArray
 	l := len(vals)
 	if l == 0 {
@@ -385,13 +385,13 @@ func (e Encoder) AppendFloats32(dst []byte, vals []float32) []byte {
 		dst = appendCborTypePrefix(dst, major, uint64(l))
 	}
 	for _, v := range vals {
-		dst = e.AppendFloat32(dst, v)
+		dst = e.AppendFloat32(dst, v, unused)
 	}
 	return dst
 }
 
 // AppendFloat64 encodes and inserts a double precision float value into the dst byte array.
-func (Encoder) AppendFloat64(dst []byte, val float64) []byte {
+func (Encoder) AppendFloat64(dst []byte, val float64, unused int) []byte {
 	switch {
 	case math.IsNaN(val):
 		return append(dst, "\xfb\x7f\xf8\x00\x00\x00\x00\x00\x00"...)
@@ -412,7 +412,7 @@ func (Encoder) AppendFloat64(dst []byte, val float64) []byte {
 }
 
 // AppendFloats64 encodes and inserts an array of double precision float values into the dst byte array.
-func (e Encoder) AppendFloats64(dst []byte, vals []float64) []byte {
+func (e Encoder) AppendFloats64(dst []byte, vals []float64, unused int) []byte {
 	major := majorTypeArray
 	l := len(vals)
 	if l == 0 {
@@ -425,7 +425,7 @@ func (e Encoder) AppendFloats64(dst []byte, vals []float64) []byte {
 		dst = appendCborTypePrefix(dst, major, uint64(l))
 	}
 	for _, v := range vals {
-		dst = e.AppendFloat64(dst, v)
+		dst = e.AppendFloat64(dst, v, unused)
 	}
 	return dst
 }
diff --git a/internal/json/time.go b/internal/json/time.go
index 6a8dc91..08cbbd9 100644
--- a/internal/json/time.go
+++ b/internal/json/time.go
@@ -88,24 +88,24 @@ func appendUnixNanoTimes(dst []byte, vals []time.Time, div int64) []byte {
 
 // AppendDuration formats the input duration with the given unit & format
 // and appends the encoded string to the input byte slice.
-func (e Encoder) AppendDuration(dst []byte, d time.Duration, unit time.Duration, useInt bool) []byte {
+func (e Encoder) AppendDuration(dst []byte, d time.Duration, unit time.Duration, useInt bool, precision int) []byte {
 	if useInt {
 		return strconv.AppendInt(dst, int64(d/unit), 10)
 	}
-	return e.AppendFloat64(dst, float64(d)/float64(unit))
+	return e.AppendFloat64(dst, float64(d)/float64(unit), precision)
 }
 
 // AppendDurations formats the input durations with the given unit & format
 // and appends the encoded string list to the input byte slice.
-func (e Encoder) AppendDurations(dst []byte, vals []time.Duration, unit time.Duration, useInt bool) []byte {
+func (e Encoder) AppendDurations(dst []byte, vals []time.Duration, unit time.Duration, useInt bool, precision int) []byte {
 	if len(vals) == 0 {
 		return append(dst, '[', ']')
 	}
 	dst = append(dst, '[')
-	dst = e.AppendDuration(dst, vals[0], unit, useInt)
+	dst = e.AppendDuration(dst, vals[0], unit, useInt, precision)
 	if len(vals) > 1 {
 		for _, d := range vals[1:] {
-			dst = e.AppendDuration(append(dst, ','), d, unit, useInt)
+			dst = e.AppendDuration(append(dst, ','), d, unit, useInt, precision)
 		}
 	}
 	dst = append(dst, ']')
diff --git a/internal/json/types.go b/internal/json/types.go
index 43feaaa..7930491 100644
--- a/internal/json/types.go
+++ b/internal/json/types.go
@@ -299,7 +299,7 @@ func (Encoder) AppendUints64(dst []byte, vals []uint64) []byte {
 	return dst
 }
 
-func appendFloat(dst []byte, val float64, bitSize int) []byte {
+func appendFloat(dst []byte, val float64, bitSize, precision int) []byte {
 	// JSON does not permit NaN or Infinity. A typical JSON encoder would fail
 	// with an error, but a logging library wants the data to get through so we
 	// make a tradeoff and store those types as string.
@@ -314,13 +314,16 @@ func appendFloat(dst []byte, val float64, bitSize int) []byte {
 	// convert as if by es6 number to string conversion
 	// see also https://cs.opensource.google/go/go/+/refs/tags/go1.20.3:src/encoding/json/encode.go;l=573
 	strFmt := byte('f')
-	// Use float32 comparisons for underlying float32 value to get precise cutoffs right.
-	if abs := math.Abs(val); abs != 0 {
-		if bitSize == 64 && (abs < 1e-6 || abs >= 1e21) || bitSize == 32 && (float32(abs) < 1e-6 || float32(abs) >= 1e21) {
-			strFmt = 'e'
+	// If precision is set to a value other than -1, we always just format the float using that precision.
+	if precision == -1 {
+		// Use float32 comparisons for underlying float32 value to get precise cutoffs right.
+		if abs := math.Abs(val); abs != 0 {
+			if bitSize == 64 && (abs < 1e-6 || abs >= 1e21) || bitSize == 32 && (float32(abs) < 1e-6 || float32(abs) >= 1e21) {
+				strFmt = 'e'
+			}
 		}
 	}
-	dst = strconv.AppendFloat(dst, val, strFmt, -1, bitSize)
+	dst = strconv.AppendFloat(dst, val, strFmt, precision, bitSize)
 	if strFmt == 'e' {
 		// Clean up e-09 to e-9
 		n := len(dst)
@@ -334,21 +337,21 @@ func appendFloat(dst []byte, val float64, bitSize int) []byte {
 
 // AppendFloat32 converts the input float32 to a string and
 // appends the encoded string to the input byte slice.
-func (Encoder) AppendFloat32(dst []byte, val float32) []byte {
-	return appendFloat(dst, float64(val), 32)
+func (Encoder) AppendFloat32(dst []byte, val float32, precision int) []byte {
+	return appendFloat(dst, float64(val), 32, precision)
 }
 
 // AppendFloats32 encodes the input float32s to json and
 // appends the encoded string list to the input byte slice.
-func (Encoder) AppendFloats32(dst []byte, vals []float32) []byte {
+func (Encoder) AppendFloats32(dst []byte, vals []float32, precision int) []byte {
 	if len(vals) == 0 {
 		return append(dst, '[', ']')
 	}
 	dst = append(dst, '[')
-	dst = appendFloat(dst, float64(vals[0]), 32)
+	dst = appendFloat(dst, float64(vals[0]), 32, precision)
 	if len(vals) > 1 {
 		for _, val := range vals[1:] {
-			dst = appendFloat(append(dst, ','), float64(val), 32)
+			dst = appendFloat(append(dst, ','), float64(val), 32, precision)
 		}
 	}
 	dst = append(dst, ']')
@@ -357,21 +360,21 @@ func (Encoder) AppendFloats32(dst []byte, vals []float32) []byte {
 
 // AppendFloat64 converts the input float64 to a string and
 // appends the encoded string to the input byte slice.
-func (Encoder) AppendFloat64(dst []byte, val float64) []byte {
-	return appendFloat(dst, val, 64)
+func (Encoder) AppendFloat64(dst []byte, val float64, precision int) []byte {
+	return appendFloat(dst, val, 64, precision)
 }
 
 // AppendFloats64 encodes the input float64s to json and
 // appends the encoded string list to the input byte slice.
-func (Encoder) AppendFloats64(dst []byte, vals []float64) []byte {
+func (Encoder) AppendFloats64(dst []byte, vals []float64, precision int) []byte {
 	if len(vals) == 0 {
 		return append(dst, '[', ']')
 	}
 	dst = append(dst, '[')
-	dst = appendFloat(dst, vals[0], 64)
+	dst = appendFloat(dst, vals[0], 64, precision)
 	if len(vals) > 1 {
 		for _, val := range vals[1:] {
-			dst = appendFloat(append(dst, ','), val, 64)
+			dst = appendFloat(append(dst, ','), val, 64, precision)
 		}
 	}
 	dst = append(dst, ']')
diff --git a/log.go b/log.go
index 9fec7cc..6c1d4ea 100644
--- a/log.go
+++ b/log.go
@@ -24,7 +24,7 @@
 //
 // Sub-loggers let you chain loggers with additional context:
 //
-//     sublogger := log.With().Str("component": "foo").Logger()
+//     sublogger := log.With().Str("component", "foo").Logger()
 //     sublogger.Info().Msg("hello world")
 //     // Output: {"time":1494567715,"level":"info","message":"hello world","component":"foo"}
 //
diff --git a/sampler.go b/sampler.go
index 1be98c4..83ce2ed 100644
--- a/sampler.go
+++ b/sampler.go
@@ -84,7 +84,7 @@ func (s *BurstSampler) Sample(lvl Level) bool {
 }
 
 func (s *BurstSampler) inc() uint32 {
-	now := time.Now().UnixNano()
+	now := TimestampFunc().UnixNano()
 	resetAt := atomic.LoadInt64(&s.resetAt)
 	var c uint32
 	if now > resetAt {


Test patch:
diff --git a/console_test.go b/console_test.go
index 08121d0..179dfee 100644
--- a/console_test.go
+++ b/console_test.go
@@ -320,6 +320,85 @@ func TestConsoleWriter(t *testing.T) {
 			t.Errorf("Unexpected output %q, want: %q", actualOutput, expectedOutput)
 		}
 	})
+
+	t.Run("With an extra 'level' field", func(t *testing.T) {
+		t.Run("malformed string", func(t *testing.T) {
+			cases := []struct {
+				field  string
+				output string
+			}{
+				{"", "<nil> ??? Hello World foo=bar\n"},
+				{"-", "<nil> - Hello World foo=bar\n"},
+				{"1", "<nil> " + zerolog.FormattedLevels[1] + " Hello World foo=bar\n"},
+				{"a", "<nil> A Hello World foo=bar\n"},
+				{"12", "<nil> 12 Hello World foo=bar\n"},
+				{"a2", "<nil> A2 Hello World foo=bar\n"},
+				{"2a", "<nil> 2A Hello World foo=bar\n"},
+				{"ab", "<nil> AB Hello World foo=bar\n"},
+				{"12a", "<nil> 12A Hello World foo=bar\n"},
+				{"a12", "<nil> A12 Hello World foo=bar\n"},
+				{"abc", "<nil> ABC Hello World foo=bar\n"},
+				{"123", "<nil> 123 Hello World foo=bar\n"},
+				{"abcd", "<nil> ABC Hello World foo=bar\n"},
+				{"1234", "<nil> 123 Hello World foo=bar\n"},
+				{"123d", "<nil> 123 Hello World foo=bar\n"},
+				{"01", "<nil> " + zerolog.FormattedLevels[1] + " Hello World foo=bar\n"},
+				{"001", "<nil> " + zerolog.FormattedLevels[1] + " Hello World foo=bar\n"},
+				{"0001", "<nil> " + zerolog.FormattedLevels[1] + " Hello World foo=bar\n"},
+			}
+			for i, c := range cases {
+				c := c
+				t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
+					buf := &bytes.Buffer{}
+					out := zerolog.NewConsoleWriter()
+					out.NoColor = true
+					out.Out = buf
+					log := zerolog.New(out)
+
+					log.Debug().Str("level", c.field).Str("foo", "bar").Msg("Hello World")
+
+					actualOutput := buf.String()
+					if actualOutput != c.output {
+						t.Errorf("Unexpected output %q, want: %q", actualOutput, c.output)
+					}
+				})
+			}
+		})
+
+		t.Run("weird value", func(t *testing.T) {
+			cases := []struct {
+				field  interface{}
+				output string
+			}{
+				{0, "<nil> 0 Hello World foo=bar\n"},
+				{1, "<nil> 1 Hello World foo=bar\n"},
+				{-1, "<nil> -1 Hello World foo=bar\n"},
+				{-3, "<nil> -3 Hello World foo=bar\n"},
+				{-32, "<nil> -32 Hello World foo=bar\n"},
+				{-321, "<nil> -32 Hello World foo=bar\n"},
+				{12, "<nil> 12 Hello World foo=bar\n"},
+				{123, "<nil> 123 Hello World foo=bar\n"},
+				{1234, "<nil> 123 Hello World foo=bar\n"},
+			}
+			for i, c := range cases {
+				c := c
+				t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
+					buf := &bytes.Buffer{}
+					out := zerolog.NewConsoleWriter()
+					out.NoColor = true
+					out.Out = buf
+					log := zerolog.New(out)
+
+					log.Debug().Interface("level", c.field).Str("foo", "bar").Msg("Hello World")
+
+					actualOutput := buf.String()
+					if actualOutput != c.output {
+						t.Errorf("Unexpected output %q, want: %q", actualOutput, c.output)
+					}
+				})
+			}
+		})
+	})
 }
 
 func TestConsoleWriterConfiguration(t *testing.T) {
@@ -407,6 +486,23 @@ func TestConsoleWriterConfiguration(t *testing.T) {
 		}
 	})
 
+	t.Run("Sets FieldsOrder", func(t *testing.T) {
+		buf := &bytes.Buffer{}
+		w := zerolog.ConsoleWriter{Out: buf, NoColor: true, FieldsOrder: []string{"zebra", "aardvark"}}
+
+		evt := `{"level": "info", "message": "Zoo", "aardvark": "Able", "mussel": "Mountain", "zebra": "Zulu"}`
+		_, err := w.Write([]byte(evt))
+		if err != nil {
+			t.Errorf("Unexpected error when writing output: %s", err)
+		}
+
+		expectedOutput := "<nil> INF Zoo zebra=Zulu aardvark=Able mussel=Mountain\n"
+		actualOutput := buf.String()
+		if actualOutput != expectedOutput {
+			t.Errorf("Unexpected output %q, want: %q", actualOutput, expectedOutput)
+		}
+	})
+
 	t.Run("Sets FieldsExclude", func(t *testing.T) {
 		buf := &bytes.Buffer{}
 		w := zerolog.ConsoleWriter{Out: buf, NoColor: true, FieldsExclude: []string{"foo"}}
diff --git a/internal/cbor/types_test.go b/internal/cbor/types_test.go
index 2fbb162..23093b1 100644
--- a/internal/cbor/types_test.go
+++ b/internal/cbor/types_test.go
@@ -169,7 +169,7 @@ var float32TestCases = []struct {
 
 func TestAppendFloat32(t *testing.T) {
 	for _, tc := range float32TestCases {
-		s := enc.AppendFloat32([]byte{}, tc.val)
+		s := enc.AppendFloat32([]byte{}, tc.val, -1)
 		got := string(s)
 		if got != tc.binary {
 			t.Errorf("AppendFloat32(%f)=0x%s, want: 0x%s",
@@ -306,9 +306,9 @@ func BenchmarkAppendFloat(b *testing.B) {
 			for i := 0; i < b.N; i++ {
 				switch str.sz {
 				case 4:
-					_ = enc.AppendFloat32(buf, float32(str.val))
+					_ = enc.AppendFloat32(buf, float32(str.val), -1)
 				case 8:
-					_ = enc.AppendFloat64(buf, str.val)
+					_ = enc.AppendFloat64(buf, str.val, -1)
 				}
 			}
 		})
diff --git a/internal/json/types_test.go b/internal/json/types_test.go
index 350affe..217cd3a 100644
--- a/internal/json/types_test.go
+++ b/internal/json/types_test.go
@@ -11,18 +11,20 @@ import (
 
 func TestAppendType(t *testing.T) {
 	w := map[string]func(interface{}) []byte{
-		"AppendInt":     func(v interface{}) []byte { return enc.AppendInt([]byte{}, v.(int)) },
-		"AppendInt8":    func(v interface{}) []byte { return enc.AppendInt8([]byte{}, v.(int8)) },
-		"AppendInt16":   func(v interface{}) []byte { return enc.AppendInt16([]byte{}, v.(int16)) },
-		"AppendInt32":   func(v interface{}) []byte { return enc.AppendInt32([]byte{}, v.(int32)) },
-		"AppendInt64":   func(v interface{}) []byte { return enc.AppendInt64([]byte{}, v.(int64)) },
-		"AppendUint":    func(v interface{}) []byte { return enc.AppendUint([]byte{}, v.(uint)) },
-		"AppendUint8":   func(v interface{}) []byte { return enc.AppendUint8([]byte{}, v.(uint8)) },
-		"AppendUint16":  func(v interface{}) []byte { return enc.AppendUint16([]byte{}, v.(uint16)) },
-		"AppendUint32":  func(v interface{}) []byte { return enc.AppendUint32([]byte{}, v.(uint32)) },
-		"AppendUint64":  func(v interface{}) []byte { return enc.AppendUint64([]byte{}, v.(uint64)) },
-		"AppendFloat32": func(v interface{}) []byte { return enc.AppendFloat32([]byte{}, v.(float32)) },
-		"AppendFloat64": func(v interface{}) []byte { return enc.AppendFloat64([]byte{}, v.(float64)) },
+		"AppendInt":                   func(v interface{}) []byte { return enc.AppendInt([]byte{}, v.(int)) },
+		"AppendInt8":                  func(v interface{}) []byte { return enc.AppendInt8([]byte{}, v.(int8)) },
+		"AppendInt16":                 func(v interface{}) []byte { return enc.AppendInt16([]byte{}, v.(int16)) },
+		"AppendInt32":                 func(v interface{}) []byte { return enc.AppendInt32([]byte{}, v.(int32)) },
+		"AppendInt64":                 func(v interface{}) []byte { return enc.AppendInt64([]byte{}, v.(int64)) },
+		"AppendUint":                  func(v interface{}) []byte { return enc.AppendUint([]byte{}, v.(uint)) },
+		"AppendUint8":                 func(v interface{}) []byte { return enc.AppendUint8([]byte{}, v.(uint8)) },
+		"AppendUint16":                func(v interface{}) []byte { return enc.AppendUint16([]byte{}, v.(uint16)) },
+		"AppendUint32":                func(v interface{}) []byte { return enc.AppendUint32([]byte{}, v.(uint32)) },
+		"AppendUint64":                func(v interface{}) []byte { return enc.AppendUint64([]byte{}, v.(uint64)) },
+		"AppendFloat32":               func(v interface{}) []byte { return enc.AppendFloat32([]byte{}, v.(float32), -1) },
+		"AppendFloat64":               func(v interface{}) []byte { return enc.AppendFloat64([]byte{}, v.(float64), -1) },
+		"AppendFloat32SmallPrecision": func(v interface{}) []byte { return enc.AppendFloat32([]byte{}, v.(float32), 1) },
+		"AppendFloat64SmallPrecision": func(v interface{}) []byte { return enc.AppendFloat64([]byte{}, v.(float64), 1) },
 	}
 	tests := []struct {
 		name  string
@@ -55,6 +57,9 @@ func TestAppendType(t *testing.T) {
 		{"AppendFloat64(-1.1)", "AppendFloat64", float64(-1.1), []byte(`-1.1`)},
 		{"AppendFloat64(1e20)", "AppendFloat64", float64(1e20), []byte(`100000000000000000000`)},
 		{"AppendFloat64(1e21)", "AppendFloat64", float64(1e21), []byte(`1e+21`)},
+
+		{"AppendFloat32SmallPrecision(-1.123)", "AppendFloat32SmallPrecision", float32(-1.123), []byte(`-1.1`)},
+		{"AppendFloat64SmallPrecision(-1.123)", "AppendFloat64SmallPrecision", float64(-1.123), []byte(`-1.1`)},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
@@ -301,7 +306,7 @@ func TestEncoder_AppendFloat64(t *testing.T) {
 	for _, tc := range float64Tests {
 		t.Run(tc.Name, func(t *testing.T) {
 			var b []byte
-			b = (Encoder{}).AppendFloat64(b, tc.Val)
+			b = (Encoder{}).AppendFloat64(b, tc.Val, -1)
 			if s := string(b); tc.Want != s {
 				t.Errorf("%q", s)
 			}
@@ -314,7 +319,7 @@ func FuzzEncoder_AppendFloat64(f *testing.F) {
 		f.Add(tc.Val)
 	}
 	f.Fuzz(func(t *testing.T, val float64) {
-		actual := (Encoder{}).AppendFloat64(nil, val)
+		actual := (Encoder{}).AppendFloat64(nil, val, -1)
 		if len(actual) == 0 {
 			t.Fatal("empty buffer")
 		}
@@ -447,7 +452,7 @@ func TestEncoder_AppendFloat32(t *testing.T) {
 	for _, tc := range float32Tests {
 		t.Run(tc.Name, func(t *testing.T) {
 			var b []byte
-			b = (Encoder{}).AppendFloat32(b, tc.Val)
+			b = (Encoder{}).AppendFloat32(b, tc.Val, -1)
 			if s := string(b); tc.Want != s {
 				t.Errorf("%q", s)
 			}
@@ -460,7 +465,7 @@ func FuzzEncoder_AppendFloat32(f *testing.F) {
 		f.Add(tc.Val)
 	}
 	f.Fuzz(func(t *testing.T, val float32) {
-		actual := (Encoder{}).AppendFloat32(nil, val)
+		actual := (Encoder{}).AppendFloat32(nil, val, -1)
 		if len(actual) == 0 {
 			t.Fatal("empty buffer")
 		}
@@ -528,7 +533,7 @@ func BenchmarkEncoder_AppendFloat32(b *testing.B) {
 
 	for i := 0; i < b.N; i++ {
 		for _, f := range floats {
-			dst = (Encoder{}).AppendFloat32(dst[:0], f)
+			dst = (Encoder{}).AppendFloat32(dst[:0], f, -1)
 		}
 	}
 }
@@ -542,7 +547,7 @@ func BenchmarkEncoder_AppendFloat64(b *testing.B) {
 
 	for i := 0; i < b.N; i++ {
 		for _, f := range floats {
-			dst = (Encoder{}).AppendFloat64(dst[:0], f)
+			dst = (Encoder{}).AppendFloat64(dst[:0], f, -1)
 		}
 	}
 }
diff --git a/log_test.go b/log_test.go
index e51eb8f..64a58be 100644
--- a/log_test.go
+++ b/log_test.go
@@ -145,6 +145,25 @@ func TestWith(t *testing.T) {
 	}
 }
 
+func TestWithReset(t *testing.T) {
+	out := &bytes.Buffer{}
+	ctx := New(out).With().
+		Str("string", "foo").
+		Stringer("stringer", net.IP{127, 0, 0, 1}).
+		Stringer("stringer_nil", nil).
+		Reset().
+		Bytes("bytes", []byte("bar")).
+		Hex("hex", []byte{0x12, 0xef}).
+		Uint64("uint64", 10).
+		Float64("float64", 12.30303).
+		Ctx(context.Background())
+	log := ctx.Logger()
+	log.Log().Msg("")
+	if got, want := decodeIfBinaryToString(out.Bytes()), `{"bytes":"bar","hex":"12ef","uint64":10,"float64":12.30303}`+"\n"; got != want {
+		t.Errorf("invalid log output:\ngot:  %v\nwant: %v", got, want)
+	}
+}
+
 func TestFieldsMap(t *testing.T) {
 	out := &bytes.Buffer{}
 	log := New(out)