Repository: rs/zerolog
Pull request #158 (2019-06-08 19:54:35)

Add TraceLevel
Sometimes you want finer-grained informational events and need to debug something that outputs way too much data to log outside of a specific build when you're targeting that particular thing and do not care about errors or other logging info (since the volume of trace info will obscure them).

I think this could encroach on loggers like journald and that's why I'm asking for your opinion on adding this new log level.


Patch:
diff --git a/README.md b/README.md
index e5008b9..718681a 100644
--- a/README.md
+++ b/README.md
@@ -82,9 +82,14 @@ func main() {
         Str("Scale", "833 cents").
         Float64("Interval", 833.09).
         Msg("Fibonacci is everywhere")
+    
+    log.Debug().
+        Str("Name", "Tom").
+        Send()
 }
 
-// Output: {"time":1524104936,"level":"debug","Scale":"833 cents","Interval":833.09,"message":"Fibonacci is everywhere"}
+// Output: {"level":"debug","Scale":"833 cents","Interval":833.09,"time":1562212768,"message":"Fibonacci is everywhere"}
+// Output: {"level":"debug","Name":"Tom","time":1562212768}
 ```
 
 > You'll note in the above example that when adding contextual fields, the fields are strongly typed. You can find the full list of supported fields [here](#standard-types)
@@ -120,6 +125,7 @@ func main() {
 * warn (`zerolog.WarnLevel`, 2)
 * info (`zerolog.InfoLevel`, 1)
 * debug (`zerolog.DebugLevel`, 0)
+* trace (`zerolog.TraceLevel`, -1)
 
 You can set the Global logging level to any of these options using the `SetGlobalLevel` function in the zerolog package, passing in one of the given constants above, e.g. `zerolog.InfoLevel` would be the "info" level.  Whichever level is chosen, all logs with a level greater than or equal to that level will be written. To turn off logging entirely, pass the `zerolog.Disabled` constant.
 
@@ -474,11 +480,10 @@ Some settings can be changed and will by 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` or `zerolog.TimeFormatUnixMs`, times are formated as UNIX timestamp.
-* DurationFieldUnit defines the unit for time.Duration type fields added using the Dur method.
-* `DurationFieldUnit`: Sets the unit of the fields added by `Dur` (default: `time.Millisecond`).
-* `DurationFieldInteger`: If set to true, `Dur` fields are formatted as integers instead of floats.
-* `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.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.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.
 
 ## Field Types
 
diff --git a/array.go b/array.go
index aa4f623..d88c497 100644
--- a/array.go
+++ b/array.go
@@ -85,6 +85,12 @@ func (a *Array) Hex(val []byte) *Array {
 	return a
 }
 
+// RawJSON adds already encoded JSON to the array.
+func (a *Array) RawJSON(val []byte) *Array {
+	a.buf = appendJSON(enc.AppendArrayDelim(a.buf), val)
+	return a
+}
+
 // Err serializes and appends the err to the array.
 func (a *Array) Err(err error) *Array {
 	marshaled := ErrorMarshalFunc(err)
diff --git a/console.go b/console.go
index 15bd063..75f6e45 100644
--- a/console.go
+++ b/console.go
@@ -317,6 +317,8 @@ func consoleDefaultFormatLevel(noColor bool) Formatter {
 		var l string
 		if ll, ok := i.(string); ok {
 			switch ll {
+			case "trace":
+				l = colorize("TRC", colorMagenta, noColor)
 			case "debug":
 				l = colorize("DBG", colorYellow, noColor)
 			case "info":
diff --git a/ctx.go b/ctx.go
index 2b7a682..ce18a32 100644
--- a/ctx.go
+++ b/ctx.go
@@ -7,6 +7,7 @@ import (
 var disabledLogger *Logger
 
 func init() {
+	SetGlobalLevel(TraceLevel)
 	l := Nop()
 	disabledLogger = &l
 }
diff --git a/event.go b/event.go
index 2707bd5..9ae7c89 100644
--- a/event.go
+++ b/event.go
@@ -105,10 +105,20 @@ func (e *Event) Msg(msg string) {
 	e.msg(msg)
 }
 
-// Msgf sends the event with formated msg added as the message field if not empty.
+// Send is equivalent to calling Msg("").
 //
-// NOTICE: once this methid is called, the *Event should be disposed.
-// Calling Msg twice can have unexpected result.
+// NOTICE: once this method is called, the *Event should be disposed.
+func (e *Event) Send() {
+	if e == nil {
+		return
+	}
+	e.msg("")
+}
+
+// Msgf sends the event with formatted msg added as the message field if not empty.
+//
+// NOTICE: once this method is called, the *Event should be disposed.
+// Calling Msgf twice can have unexpected result.
 func (e *Event) Msgf(format string, v ...interface{}) {
 	if e == nil {
 		return
@@ -117,13 +127,8 @@ func (e *Event) Msgf(format string, v ...interface{}) {
 }
 
 func (e *Event) msg(msg string) {
-	if len(e.ch) > 0 {
-		e.ch[0].Run(e, e.level, msg)
-		if len(e.ch) > 1 {
-			for _, hook := range e.ch[1:] {
-				hook.Run(e, e.level, msg)
-			}
-		}
+	for _, hook := range e.ch {
+		hook.Run(e, e.level, msg)
 	}
 	if msg != "" {
 		e.buf = enc.AppendString(enc.AppendKey(e.buf, MessageFieldName), msg)
@@ -203,7 +208,7 @@ func (e *Event) Object(key string, obj LogObjectMarshaler) *Event {
 	return e
 }
 
-// Object marshals an object that implement the LogObjectMarshaler interface.
+// EmbedObject marshals an object that implement the LogObjectMarshaler interface.
 func (e *Event) EmbedObject(obj LogObjectMarshaler) *Event {
 	if e == nil {
 		return e
diff --git a/globals.go b/globals.go
index c1fa37c..421429a 100644
--- a/globals.go
+++ b/globals.go
@@ -2,18 +2,22 @@ package zerolog
 
 import (
 	"strconv"
+	"sync/atomic"
 	"time"
 )
-import "sync/atomic"
 
 const (
 	// TimeFormatUnix defines a time format that makes time fields to be
 	// serialized as Unix timestamp integers.
 	TimeFormatUnix = ""
 
-	// TimeFormatUnix defines a time format that makes time fields to be
+	// TimeFormatUnixMs defines a time format that makes time fields to be
 	// serialized as Unix timestamp integers in milliseconds.
 	TimeFormatUnixMs = "UNIXMS"
+
+	// TimeFormatUnixMicro defines a time format that makes time fields to be
+	// serialized as Unix timestamp integers in microseconds.
+	TimeFormatUnixMicro = "UNIXMICRO"
 )
 
 var (
@@ -57,7 +61,7 @@ var (
 	}
 
 	// TimeFieldFormat defines the time format of the Time field type. If set to
-	// TimeFormatUnix or TimeFormatUnixMs, the time is formatted as an UNIX
+	// TimeFormatUnix, TimeFormatUnixMs or TimeFormatUnixMicro, the time is formatted as an UNIX
 	// timestamp as integer.
 	TimeFieldFormat = time.RFC3339
 
@@ -79,8 +83,8 @@ var (
 )
 
 var (
-	gLevel          = new(uint32)
-	disableSampling = new(uint32)
+	gLevel          = new(int32)
+	disableSampling = new(int32)
 )
 
 // SetGlobalLevel sets the global override for log level. If this
@@ -88,23 +92,23 @@ var (
 //
 // To globally disable logs, set GlobalLevel to Disabled.
 func SetGlobalLevel(l Level) {
-	atomic.StoreUint32(gLevel, uint32(l))
+	atomic.StoreInt32(gLevel, int32(l))
 }
 
 // GlobalLevel returns the current global log level
 func GlobalLevel() Level {
-	return Level(atomic.LoadUint32(gLevel))
+	return Level(atomic.LoadInt32(gLevel))
 }
 
 // DisableSampling will disable sampling in all Loggers if true.
 func DisableSampling(v bool) {
-	var i uint32
+	var i int32
 	if v {
 		i = 1
 	}
-	atomic.StoreUint32(disableSampling, i)
+	atomic.StoreInt32(disableSampling, i)
 }
 
 func samplingDisabled() bool {
-	return atomic.LoadUint32(disableSampling) == 1
+	return atomic.LoadInt32(disableSampling) == 1
 }
diff --git a/go.mod b/go.mod
index 0275fdd..8c42ba8 100644
--- a/go.mod
+++ b/go.mod
@@ -5,5 +5,5 @@ require (
 	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-20190425163242-31fd60d6bfdc
+	golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74
 )
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..b14fd2b
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,16 @@
+github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e h1:Wf6HqHfScWJN9/ZjdUKyjop4mf3Qdd+1TvvltAvM3m8=
+github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+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=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74 h1:4cFkmztxtMslUX2SctSl+blCyXfpzhGOy9LhKAqSMA4=
+golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/hlog/hlog.go b/hlog/hlog.go
index f7c3a5a..37f4336 100644
--- a/hlog/hlog.go
+++ b/hlog/hlog.go
@@ -171,6 +171,22 @@ func RequestIDHandler(fieldKey, headerName string) func(next http.Handler) http.
 	}
 }
 
+// CustomHeaderHandler adds given header from request's header as a field to
+// the context's logger using fieldKey as field key.
+func CustomHeaderHandler(fieldKey, header string) func(next http.Handler) http.Handler {
+	return func(next http.Handler) http.Handler {
+		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			if val := r.Header.Get(header); val != "" {
+				log := zerolog.Ctx(r.Context())
+				log.UpdateContext(func(c zerolog.Context) zerolog.Context {
+					return c.Str(fieldKey, val)
+				})
+			}
+			next.ServeHTTP(w, r)
+		})
+	}
+}
+
 // AccessHandler returns a handler that call f after each request.
 func AccessHandler(f func(r *http.Request, status, size int, duration time.Duration)) func(next http.Handler) http.Handler {
 	return func(next http.Handler) http.Handler {
diff --git a/hook.go b/hook.go
index 08133ac..ec6effc 100644
--- a/hook.go
+++ b/hook.go
@@ -17,12 +17,16 @@ func (h HookFunc) Run(e *Event, level Level, message string) {
 
 // LevelHook applies a different hook for each level.
 type LevelHook struct {
-	NoLevelHook, DebugHook, InfoHook, WarnHook, ErrorHook, FatalHook, PanicHook Hook
+	NoLevelHook, TraceHook, DebugHook, InfoHook, WarnHook, ErrorHook, FatalHook, PanicHook Hook
 }
 
 // Run implements the Hook interface.
 func (h LevelHook) Run(e *Event, level Level, message string) {
 	switch level {
+	case TraceLevel:
+		if h.TraceHook != nil {
+			h.TraceHook.Run(e, level, message)
+		}
 	case DebugLevel:
 		if h.DebugHook != nil {
 			h.DebugHook.Run(e, level, message)
diff --git a/internal/cbor/types.go b/internal/cbor/types.go
index eb4f697..3d76ea0 100644
--- a/internal/cbor/types.go
+++ b/internal/cbor/types.go
@@ -24,9 +24,9 @@ func (Encoder) AppendEndMarker(dst []byte) []byte {
 
 // AppendObjectData takes an object in form of a byte array and appends to dst.
 func (Encoder) AppendObjectData(dst []byte, o []byte) []byte {
-        // BeginMarker is present in the dst, which
-        // should not be copied when appending to existing data.
-        return append(dst, o[1:]...)
+	// BeginMarker is present in the dst, which
+	// should not be copied when appending to existing data.
+	return append(dst, o[1:]...)
 }
 
 // AppendArrayStart adds markers to indicate the start of an array.
diff --git a/internal/json/time.go b/internal/json/time.go
index 18dea5e..5aff6be 100644
--- a/internal/json/time.go
+++ b/internal/json/time.go
@@ -9,6 +9,7 @@ const (
 	// Import from zerolog/global.go
 	timeFormatUnix   = ""
 	timeFormatUnixMs = "UNIXMS"
+	timeFormatUnixMicro = "UNIXMICRO"
 )
 
 // AppendTime formats the input time with the given format
@@ -19,6 +20,8 @@ func (e Encoder) AppendTime(dst []byte, t time.Time, format string) []byte {
 		return e.AppendInt64(dst, t.Unix())
 	case timeFormatUnixMs:
 		return e.AppendInt64(dst, t.UnixNano()/1000000)
+	case timeFormatUnixMicro:
+		return e.AppendInt64(dst, t.UnixNano()/1000)
 	}
 	return append(t.AppendFormat(append(dst, '"'), format), '"')
 }
diff --git a/journald/journald.go b/journald/journald.go
index dde4e3e..058a68a 100644
--- a/journald/journald.go
+++ b/journald/journald.go
@@ -20,11 +20,12 @@ import (
 	"bytes"
 	"encoding/json"
 	"fmt"
+	"io"
+	"strings"
+
 	"github.com/coreos/go-systemd/journal"
 	"github.com/rs/zerolog"
 	"github.com/rs/zerolog/internal/cbor"
-	"io"
-	"strings"
 )
 
 const defaultJournalDPrio = journal.PriNotice
@@ -47,6 +48,8 @@ func levelToJPrio(zLevel string) journal.Priority {
 	lvl, _ := zerolog.ParseLevel(zLevel)
 
 	switch lvl {
+	case zerolog.TraceLevel:
+		return journal.PriDebug
 	case zerolog.DebugLevel:
 		return journal.PriDebug
 	case zerolog.InfoLevel:
@@ -67,7 +70,7 @@ func levelToJPrio(zLevel string) journal.Priority {
 
 func (w journalWriter) Write(p []byte) (n int, err error) {
 	if !journal.Enabled() {
-		err = fmt.Errorf("Cannot connect to journalD!!")
+		err = fmt.Errorf("cannot connect to journalD")
 		return
 	}
 	var event map[string]interface{}
diff --git a/log.go b/log.go
index 21eb8e4..c0507de 100644
--- a/log.go
+++ b/log.go
@@ -107,9 +107,11 @@ import (
 )
 
 // Level defines log levels.
-type Level uint8
+type Level int8
 
 const (
+	// TraceLevel defines trace log level.
+	TraceLevel Level = -1
 	// DebugLevel defines debug log level.
 	DebugLevel Level = iota
 	// InfoLevel defines info log level.
@@ -130,6 +132,8 @@ const (
 
 func (l Level) String() string {
 	switch l {
+	case TraceLevel:
+		return "trace"
 	case DebugLevel:
 		return "debug"
 	case InfoLevel:
@@ -152,6 +156,8 @@ func (l Level) String() string {
 // returns an error if the input string does not match known values.
 func ParseLevel(levelStr string) (Level, error) {
 	switch levelStr {
+	case LevelFieldMarshalFunc(TraceLevel):
+		return TraceLevel, nil
 	case LevelFieldMarshalFunc(DebugLevel):
 		return DebugLevel, nil
 	case LevelFieldMarshalFunc(InfoLevel):
@@ -198,7 +204,7 @@ func New(w io.Writer) Logger {
 	if !ok {
 		lw = levelWriterAdapter{w}
 	}
-	return Logger{w: lw}
+	return Logger{w: lw, level: TraceLevel}
 }
 
 // Nop returns a disabled logger for which all operation are no-op.
@@ -251,6 +257,11 @@ func (l Logger) Level(lvl Level) Logger {
 	return l
 }
 
+// GetLevel returns the current Level of l.
+func (l Logger) GetLevel() Level {
+	return l.level
+}
+
 // Sample returns a logger with the s sampler.
 func (l Logger) Sample(s Sampler) Logger {
 	l.sampler = s
@@ -263,6 +274,13 @@ func (l Logger) Hook(h Hook) Logger {
 	return l
 }
 
+// Trace starts a new message with trace level.
+//
+// You must call Msg on the returned event in order to send the event.
+func (l *Logger) Trace() *Event {
+	return l.newEvent(TraceLevel, nil)
+}
+
 // Debug starts a new message with debug level.
 //
 // You must call Msg on the returned event in order to send the event.
@@ -298,9 +316,9 @@ func (l *Logger) Error() *Event {
 func (l *Logger) Err(err error) *Event {
 	if err != nil {
 		return l.Error().Err(err)
-	} else {
-		return l.Info()
 	}
+
+	return l.Info()
 }
 
 // Fatal starts a new message with fatal level. The os.Exit(1) function
@@ -326,6 +344,8 @@ func (l *Logger) Panic() *Event {
 // You must call Msg on the returned event in order to send the event.
 func (l *Logger) WithLevel(level Level) *Event {
 	switch level {
+	case TraceLevel:
+		return l.Trace()
 	case DebugLevel:
 		return l.Debug()
 	case InfoLevel:
diff --git a/log/log.go b/log/log.go
index dd92ab9..d3e12d0 100644
--- a/log/log.go
+++ b/log/log.go
@@ -37,6 +37,13 @@ func Hook(h zerolog.Hook) zerolog.Logger {
 	return Logger.Hook(h)
 }
 
+// Trace starts a new message with trace level.
+//
+// You must call Msg on the returned event in order to send the event.
+func Trace() *zerolog.Event {
+	return Logger.Trace()
+}
+
 // Debug starts a new message with debug level.
 //
 // You must call Msg on the returned event in order to send the event.
diff --git a/sampler.go b/sampler.go
index 7b0923e..a99629e 100644
--- a/sampler.go
+++ b/sampler.go
@@ -104,11 +104,15 @@ func (s *BurstSampler) inc() uint32 {
 
 // LevelSampler applies a different sampler for each level.
 type LevelSampler struct {
-	DebugSampler, InfoSampler, WarnSampler, ErrorSampler Sampler
+	TraceSampler, DebugSampler, InfoSampler, WarnSampler, ErrorSampler Sampler
 }
 
 func (s LevelSampler) Sample(lvl Level) bool {
 	switch lvl {
+	case TraceLevel:
+		if s.TraceSampler != nil {
+			return s.TraceSampler.Sample(lvl)
+		}
 	case DebugLevel:
 		if s.DebugSampler != nil {
 			return s.DebugSampler.Sample(lvl)
diff --git a/syslog.go b/syslog.go
index 82b470e..31c97a5 100644
--- a/syslog.go
+++ b/syslog.go
@@ -10,6 +10,7 @@ import (
 // SyslogWriter is an interface matching a syslog.Writer struct.
 type SyslogWriter interface {
 	io.Writer
+	Trace(m string) error
 	Debug(m string) error
 	Info(m string) error
 	Warning(m string) error
@@ -35,6 +36,8 @@ func (sw syslogWriter) Write(p []byte) (n int, err error) {
 // WriteLevel implements LevelWriter interface.
 func (sw syslogWriter) WriteLevel(level Level, p []byte) (n int, err error) {
 	switch level {
+	case TraceLevel:
+		err = sw.w.Trace(string(p))
 	case DebugLevel:
 		err = sw.w.Debug(string(p))
 	case InfoLevel:


Test patch:
diff --git a/array_test.go b/array_test.go
index 66c6668..e8857e6 100644
--- a/array_test.go
+++ b/array_test.go
@@ -24,10 +24,11 @@ func TestArray(t *testing.T) {
 		Str("a").
 		Bytes([]byte("b")).
 		Hex([]byte{0x1f}).
+		RawJSON([]byte(`{"some":"json"}`)).
 		Time(time.Time{}).
 		IPAddr(net.IP{192, 168, 0, 10}).
 		Dur(0)
-	want := `[true,1,2,3,4,5,6,7,8,9,10,11.98122,12.987654321,"a","b","1f","0001-01-01T00:00:00Z","192.168.0.10",0]`
+	want := `[true,1,2,3,4,5,6,7,8,9,10,11.98122,12.987654321,"a","b","1f",{"some":"json"},"0001-01-01T00:00:00Z","192.168.0.10",0]`
 	if got := decodeObjectToStr(a.write([]byte{})); got != want {
 		t.Errorf("Array.write()\ngot:  %s\nwant: %s", got, want)
 	}
diff --git a/binary_test.go b/binary_test.go
index b4b52d9..79cfa9c 100644
--- a/binary_test.go
+++ b/binary_test.go
@@ -108,6 +108,19 @@ func ExampleLogger_Printf() {
 	// Output: {"level":"debug","message":"hello world"}
 }
 
+func ExampleLogger_Trace() {
+	dst := bytes.Buffer{}
+	log := New(&dst)
+
+	log.Trace().
+		Str("foo", "bar").
+		Int("n", 123).
+		Msg("hello world")
+
+	fmt.Println(decodeIfBinaryToString(dst.Bytes()))
+	// Output: {"level":"trace","foo":"bar","n":123,"message":"hello world"}
+}
+
 func ExampleLogger_Debug() {
 	dst := bytes.Buffer{}
 	log := New(&dst)
diff --git a/hlog/hlog_test.go b/hlog/hlog_test.go
index 975ce38..2967eb2 100644
--- a/hlog/hlog_test.go
+++ b/hlog/hlog_test.go
@@ -182,6 +182,24 @@ func TestRequestIDHandler(t *testing.T) {
 	h.ServeHTTP(httptest.NewRecorder(), r)
 }
 
+func TestCustomHeaderHandler(t *testing.T) {
+	out := &bytes.Buffer{}
+	r := &http.Request{
+		Header: http.Header{
+			"X-Request-Id": []string{"514bbe5bb5251c92bd07a9846f4a1ab6"},
+		},
+	}
+	h := CustomHeaderHandler("reqID", "X-Request-Id")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		l := FromRequest(r)
+		l.Log().Msg("")
+	}))
+	h = NewHandler(zerolog.New(out))(h)
+	h.ServeHTTP(nil, r)
+	if want, got := `{"reqID":"514bbe5bb5251c92bd07a9846f4a1ab6"}`+"\n", decodeIfBinary(out); want != got {
+		t.Errorf("Invalid log output, got: %s, want: %s", got, want)
+	}
+}
+
 func TestCombinedHandlers(t *testing.T) {
 	out := &bytes.Buffer{}
 	r := &http.Request{
diff --git a/log/log_example_test.go b/log/log_example_test.go
index 4938435..3127e51 100644
--- a/log/log_example_test.go
+++ b/log/log_example_test.go
@@ -54,6 +54,14 @@ func ExampleLog() {
 	// Output: {"time":1199811905,"message":"hello world"}
 }
 
+// Example of a log at a particular "level" (in this case, "trace")
+func ExampleTrace() {
+	setup()
+	log.Trace().Msg("hello world")
+
+	// Output: {"level":"trace","time":1199811905,"message":"hello world"}
+}
+
 // Example of a log at a particular "level" (in this case, "debug")
 func ExampleDebug() {
 	setup()
diff --git a/log_example_test.go b/log_example_test.go
index 7344745..71fd217 100644
--- a/log_example_test.go
+++ b/log_example_test.go
@@ -95,6 +95,17 @@ func ExampleLogger_Printf() {
 	// Output: {"level":"debug","message":"hello world"}
 }
 
+func ExampleLogger_Trace() {
+	log := zerolog.New(os.Stdout)
+
+	log.Trace().
+		Str("foo", "bar").
+		Int("n", 123).
+		Msg("hello world")
+
+	// Output: {"level":"trace","foo":"bar","n":123,"message":"hello world"}
+}
+
 func ExampleLogger_Debug() {
 	log := zerolog.New(os.Stdout)
 
diff --git a/log_test.go b/log_test.go
index c7891e4..dee2916 100644
--- a/log_test.go
+++ b/log_test.go
@@ -453,6 +453,24 @@ func TestLevel(t *testing.T) {
 	})
 }
 
+func TestGetLevel(t *testing.T) {
+	levels := []Level{
+		DebugLevel,
+		InfoLevel,
+		WarnLevel,
+		ErrorLevel,
+		FatalLevel,
+		PanicLevel,
+		NoLevel,
+		Disabled,
+	}
+	for _, level := range levels {
+		if got, want := New(nil).Level(level).GetLevel(), level; got != want {
+			t.Errorf("GetLevel() = %v, want: %v", got, want)
+		}
+	}
+}
+
 func TestSampling(t *testing.T) {
 	out := &bytes.Buffer{}
 	log := New(out).Sample(&BasicSampler{N: 2})
@@ -508,30 +526,34 @@ func TestLevelWriter(t *testing.T) {
 		}{},
 	}
 	log := New(lw)
+	log.Trace().Msg("0")
 	log.Debug().Msg("1")
 	log.Info().Msg("2")
 	log.Warn().Msg("3")
 	log.Error().Msg("4")
 	log.Log().Msg("nolevel-1")
-	log.WithLevel(DebugLevel).Msg("5")
-	log.WithLevel(InfoLevel).Msg("6")
-	log.WithLevel(WarnLevel).Msg("7")
-	log.WithLevel(ErrorLevel).Msg("8")
+	log.WithLevel(TraceLevel).Msg("5")
+	log.WithLevel(DebugLevel).Msg("6")
+	log.WithLevel(InfoLevel).Msg("7")
+	log.WithLevel(WarnLevel).Msg("8")
+	log.WithLevel(ErrorLevel).Msg("9")
 	log.WithLevel(NoLevel).Msg("nolevel-2")
 
 	want := []struct {
 		l Level
 		p string
 	}{
+		{TraceLevel, `{"level":"trace","message":"0"}` + "\n"},
 		{DebugLevel, `{"level":"debug","message":"1"}` + "\n"},
 		{InfoLevel, `{"level":"info","message":"2"}` + "\n"},
 		{WarnLevel, `{"level":"warn","message":"3"}` + "\n"},
 		{ErrorLevel, `{"level":"error","message":"4"}` + "\n"},
 		{NoLevel, `{"message":"nolevel-1"}` + "\n"},
-		{DebugLevel, `{"level":"debug","message":"5"}` + "\n"},
-		{InfoLevel, `{"level":"info","message":"6"}` + "\n"},
-		{WarnLevel, `{"level":"warn","message":"7"}` + "\n"},
-		{ErrorLevel, `{"level":"error","message":"8"}` + "\n"},
+		{TraceLevel, `{"level":"trace","message":"5"}` + "\n"},
+		{DebugLevel, `{"level":"debug","message":"6"}` + "\n"},
+		{InfoLevel, `{"level":"info","message":"7"}` + "\n"},
+		{WarnLevel, `{"level":"warn","message":"8"}` + "\n"},
+		{ErrorLevel, `{"level":"error","message":"9"}` + "\n"},
 		{NoLevel, `{"message":"nolevel-2"}` + "\n"},
 	}
 	if got := lw.ops; !reflect.DeepEqual(got, want) {
@@ -678,9 +700,9 @@ func TestCallerMarshalFunc(t *testing.T) {
 		parts := strings.Split(file, "/")
 		if len(parts) > 1 {
 			return strings.Join(parts[len(parts)-2:], "/") + ":" + strconv.Itoa(line)
-		} else {
-			return file + ":" + strconv.Itoa(line)
 		}
+
+		return file + ":" + strconv.Itoa(line)
 	}
 	_, file, line, _ = runtime.Caller(0)
 	caller = CallerMarshalFunc(file, line+2)
diff --git a/syslog_test.go b/syslog_test.go
index 94d15d9..aa8d41e 100644
--- a/syslog_test.go
+++ b/syslog_test.go
@@ -17,6 +17,10 @@ type syslogTestWriter struct {
 func (w *syslogTestWriter) Write(p []byte) (int, error) {
 	return 0, nil
 }
+func (w *syslogTestWriter) Trace(m string) error {
+	w.events = append(w.events, syslogEvent{"Trace", m})
+	return nil
+}
 func (w *syslogTestWriter) Debug(m string) error {
 	w.events = append(w.events, syslogEvent{"Debug", m})
 	return nil
@@ -45,12 +49,14 @@ func (w *syslogTestWriter) Crit(m string) error {
 func TestSyslogWriter(t *testing.T) {
 	sw := &syslogTestWriter{}
 	log := New(SyslogLevelWriter(sw))
+	log.Trace().Msg("trace")
 	log.Debug().Msg("debug")
 	log.Info().Msg("info")
 	log.Warn().Msg("warn")
 	log.Error().Msg("error")
 	log.Log().Msg("nolevel")
 	want := []syslogEvent{
+		{"Trace", `{"level":"trace","message":"trace"}` + "\n"},
 		{"Debug", `{"level":"debug","message":"debug"}` + "\n"},
 		{"Info", `{"level":"info","message":"info"}` + "\n"},
 		{"Warning", `{"level":"warn","message":"warn"}` + "\n"},