Initial commit

This commit is contained in:
Olivier Poitrey 2017-05-11 22:24:39 -07:00
commit 7f302b00ec
15 changed files with 1546 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof

10
.travis.yml Normal file
View File

@ -0,0 +1,10 @@
language: go
go:
- 1.7
- 1.8
- tip
matrix:
allow_failures:
- go: tip
script:
go test -v -race -cpu=1,2,4 ./...

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 Olivier Poitrey
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.

165
README.md Normal file
View File

@ -0,0 +1,165 @@
# Zero Allocation JSON Logger
[![godoc](http://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/github.com/rs/zerolog) [![license](http://img.shields.io/badge/license-MIT-red.svg?style=flat)](https://raw.githubusercontent.com/rs/zerolog/master/LICENSE) [![Build Status](https://travis-ci.org/rs/zerolog.svg?branch=master)](https://travis-ci.org/rs/zerolog) [![Coverage](http://gocover.io/_badge/github.com/rs/zerolog)](http://gocover.io/github.com/rs/zerolog)
The zerolog package provides a fast and simple logger dedicated to JSON output. It is inspired by uber's [zap](https://godoc.org/go.uber.org/zap) but with a mutch simpler to use API and smaller code base.
## Features
* Level logging
* Sampling
* Contextual fields
## Benchmark
All operations are allocation free:
```
BenchmarkLogEmpty-8 50000000 22 ns/op 0 B/op 0 allocs/op
BenchmarkDisabled-8 100000000 10 ns/op 0 B/op 0 allocs/op
BenchmarkInfo-8 10000000 210 ns/op 0 B/op 0 allocs/op
BenchmarkContextFields-8 10000000 254 ns/op 0 B/op 0 allocs/op
BenchmarkLogFields-8 5000000 377 ns/op 0 B/op 0 allocs/op
```
## Usage
```go
import "github.com/rs/zerolog/log"
```
### A global logger can be use for simple logging
```go
log.Info().Msg("hello world")
// Output: {"level":"info","time":1494567715,"message":"hello world"}
```
NOTE: To import the global logger, import the `log` subpackage `github.com/rs/zerolog/log`.
```go
log.Fatal().
Err(err).
Str("service", service).
Msgf("Cannot start %s", service)
// Output: {"level":"fatal","time":1494567715,"message":"Cannot start myservice","error":"some error","service":"myservice"}
// Exit 1
```
NOTE: Using `Msgf` generates an allocation even when the logger is disabled.
### Fields can be added to log messages
```go
log.Info().
Str("foo", "bar").
Int("n", 123).
Msg("hello world")
// Output: {"level":"info","time":1494567715,"foo":"bar","n":123,"message":"hello world"}
```
### Create logger instance to manage different outputs
```go
logger := zerolog.New(os.Stderr).With().Timestamp().Logger()
logger.Info().Str("foo", "bar").Msg("hello world")
// Output: {"level":"info","time":1494567715,"message":"hello world","foo":"bar"}
```
### Sub-loggers let you chain loggers with additional context
```go
sublogger := log.With().
Str("component": "foo").
Logger()
sublogger.Info().Msg("hello world")
// Output: {"level":"info","time":1494567715,"message":"hello world","component":"foo"}
```
### Level logging
```go
zerolog.GlobalLevel = zerolog.InfoLevel
log.Debug().Msg("filtered out message")
log.Info().Msg("routed message")
if e := log.Debug(); e.Enabled() {
// Compute log output only if enabled.
value := compute()
e.Str("foo": value).Msg("some debug message")
}
// Output: {"level":"info","time":1494567715,"routed message"}
```
### Customize automatic field names
```go
zerolog.TimestampFieldName = "t"
zerolog.LevelFieldName = "l"
zerolog.MessageFieldName = "m"
log.Info().Msg("hello world")
// Output: {"l":"info","t":1494567715,"m":"hello world"}
```
### Log with no level nor message
```go
log.Log().Str("foo","bar").Msg("")
// Output: {"time":1494567715,"foo":"bar"}
```
### Add contextual fields to the global logger
```go
log.Logger = log.With().Str("foo", "bar").Logger()
```
### Log Sampling
```go
sampled := log.Sample(10)
sampled.Info().Msg("will be logged every 10 messages")
```
## 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.GlobalLevel`: Can raise the mimimum level of all loggers. Set this to `zerolog.Disable` to disable logging altogether (quiet mode).
* `zerolog.SamplingDisable`: If set to `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.
* `zerolog.MessageFieldName`: Can be set to customize message field name.
* `zerolog.ErrorFieldName`: Can be set to customize `Err` field name.
* `zerolog.SampleFieldName`: Can be set to customize the field name added when sampling is enabled.
* `zerolog.TimeFieldFormat`: Can be set to customize `Time` field value formatting.
## Field Types
### Standard Types
* `Str`
* `Bool`
* `Int`, `Int8`, `Int16`, `Int32`, `Int64`
* `Uint`, `Uint8`, `Uint16`, `Uint32`, `Uint64`
* `Float32`, `Float64`
### Advanced Fields
* `Timestamp`: Insert UNIX timestamp field with `zerolog.TimestampFieldName` field name.
* `Time`: Add a field with the time formated with the `zerolog.TimeFieldFormat`.
* `Err`: Takes an `error` and render it as a string using the `zerolog.ErrorFieldName` field name.

72
benchmark_test.go Normal file
View File

@ -0,0 +1,72 @@
package zerolog
import (
"errors"
"io/ioutil"
"testing"
)
var (
errExample = errors.New("fail")
fakeMessage = "Test logging, but use a somewhat realistic message length."
)
func BenchmarkLogEmpty(b *testing.B) {
logger := New(ioutil.Discard)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
logger.Log().Msg("")
}
})
}
func BenchmarkDisabled(b *testing.B) {
logger := New(ioutil.Discard).Level(Disabled)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
logger.Info().Msg(fakeMessage)
}
})
}
func BenchmarkInfo(b *testing.B) {
logger := New(ioutil.Discard)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
logger.Info().Msg(fakeMessage)
}
})
}
func BenchmarkContextFields(b *testing.B) {
logger := New(ioutil.Discard).With().
Str("string", "four!").
Str("time", "now"). // XXX
Str("duration", "123"). //XXX
Str("another string", "done!").
Logger()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
logger.Info().Msg(fakeMessage)
}
})
}
func BenchmarkLogFields(b *testing.B) {
logger := New(ioutil.Discard)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
logger.Info().
Str("string", "four!").
Str("time", "now"). // XXX
Str("duration", "123"). //XXX
Str("another string", "done!").
Msg(fakeMessage)
}
})
}

94
context.go Normal file
View File

@ -0,0 +1,94 @@
package zerolog
import "time"
// Context configures a new sub-logger with contextual fields.
type Context struct {
l Logger
}
// Logger returns the logger with the context previously set.
func (c Context) Logger() Logger {
return c.l
}
func (c Context) append(f field) Context {
return Context{
l: Logger{
parent: c.l,
w: c.l.w,
field: f.compileJSON(),
level: c.l.level,
sample: c.l.sample,
counter: c.l.counter,
},
}
}
func (c Context) Str(key, val string) Context {
return c.append(fStr(key, val))
}
func (c Context) Err(err error) Context {
return c.append(fErr(err))
}
func (c Context) Bool(key string, b bool) Context {
return c.append(fBool(key, b))
}
func (c Context) Int(key string, i int) Context {
return c.append(fInt(key, i))
}
func (c Context) Int8(key string, i int8) Context {
return c.append(fInt8(key, i))
}
func (c Context) Int16(key string, i int16) Context {
return c.append(fInt16(key, i))
}
func (c Context) Int32(key string, i int32) Context {
return c.append(fInt32(key, i))
}
func (c Context) Int64(key string, i int64) Context {
return c.append(fInt64(key, i))
}
func (c Context) Uint(key string, i uint) Context {
return c.append(fUint(key, i))
}
func (c Context) Uint8(key string, i uint8) Context {
return c.append(fUint8(key, i))
}
func (c Context) Uint16(key string, i uint16) Context {
return c.append(fUint16(key, i))
}
func (c Context) Uint32(key string, i uint32) Context {
return c.append(fUint32(key, i))
}
func (c Context) Uint64(key string, i uint64) Context {
return c.append(fUint64(key, i))
}
func (c Context) Float32(key string, f float32) Context {
return c.append(fFloat32(key, f))
}
func (c Context) Float64(key string, f float64) Context {
return c.append(fFloat64(key, f))
}
func (c Context) Timestamp() Context {
return c.append(fTimestamp())
}
func (c Context) Time(key string, t time.Time) Context {
return c.append(fTime(key, t))
}

213
event.go Normal file
View File

@ -0,0 +1,213 @@
package zerolog
import (
"bytes"
"fmt"
"sync"
"time"
)
var pool = &sync.Pool{
New: func() interface{} {
return bytes.NewBuffer(make([]byte, 500))
},
}
type Event struct {
buf *bytes.Buffer
w LevelWriter
level Level
enabled bool
done func(msg string)
}
func newEvent(w LevelWriter, level Level, enabled bool) Event {
if !enabled {
return Event{}
}
buf := pool.Get().(*bytes.Buffer)
buf.Reset()
buf.WriteByte('{')
return Event{
buf: buf,
w: w,
level: level,
enabled: true,
}
}
func (e Event) write() (n int, err error) {
if !e.enabled {
return 0, nil
}
e.buf.WriteByte('}')
e.buf.WriteByte('\n')
n, err = e.w.WriteLevel(e.level, e.buf.Bytes())
pool.Put(e.buf)
return
}
func (e Event) append(f field) Event {
if !e.enabled {
return e
}
if e.buf.Len() > 1 {
e.buf.WriteByte(',')
}
f.writeJSON(e.buf)
return e
}
// Enabled return false if the event is going to be filtered out by
// log level or sampling.
func (e Event) Enabled() bool {
return e.enabled
}
// Msg sends the event with msg added as the message field if not empty.
func (e Event) Msg(msg string) (n int, err error) {
if !e.enabled {
return 0, nil
}
if msg != "" {
e.append(fStr(MessageFieldName, msg))
}
if e.done != nil {
defer e.done(msg)
}
return e.write()
}
// Msgf sends the event with formated msg added as the message field if not empty.
func (e Event) Msgf(format string, v ...interface{}) (n int, err error) {
if !e.enabled {
return 0, nil
}
msg := fmt.Sprintf(format, v...)
if msg != "" {
e.append(fStr(MessageFieldName, msg))
}
if e.done != nil {
defer e.done(msg)
}
return e.write()
}
func (e Event) Str(key, val string) Event {
if !e.enabled {
return e
}
return e.append(fStr(key, val))
}
func (e Event) Err(err error) Event {
if !e.enabled {
return e
}
return e.append(fErr(err))
}
func (e Event) Bool(key string, b bool) Event {
if !e.enabled {
return e
}
return e.append(fBool(key, b))
}
func (e Event) Int(key string, i int) Event {
if !e.enabled {
return e
}
return e.append(fInt(key, i))
}
func (e Event) Int8(key string, i int8) Event {
if !e.enabled {
return e
}
return e.append(fInt8(key, i))
}
func (e Event) Int16(key string, i int16) Event {
if !e.enabled {
return e
}
return e.append(fInt16(key, i))
}
func (e Event) Int32(key string, i int32) Event {
if !e.enabled {
return e
}
return e.append(fInt32(key, i))
}
func (e Event) Int64(key string, i int64) Event {
if !e.enabled {
return e
}
return e.append(fInt64(key, i))
}
func (e Event) Uint(key string, i uint) Event {
if !e.enabled {
return e
}
return e.append(fUint(key, i))
}
func (e Event) Uint8(key string, i uint8) Event {
if !e.enabled {
return e
}
return e.append(fUint8(key, i))
}
func (e Event) Uint16(key string, i uint16) Event {
if !e.enabled {
return e
}
return e.append(fUint16(key, i))
}
func (e Event) Uint32(key string, i uint32) Event {
if !e.enabled {
return e
}
return e.append(fUint32(key, i))
}
func (e Event) Uint64(key string, i uint64) Event {
if !e.enabled {
return e
}
return e.append(fUint64(key, i))
}
func (e Event) Float32(key string, f float32) Event {
if !e.enabled {
return e
}
return e.append(fFloat32(key, f))
}
func (e Event) Float64(key string, f float64) Event {
if !e.enabled {
return e
}
return e.append(fFloat64(key, f))
}
func (e Event) Timestamp() Event {
if !e.enabled {
return e
}
return e.append(fTimestamp())
}
func (e Event) Time(key string, t time.Time) Event {
if !e.enabled {
return e
}
return e.append(fTime(key, t))
}

158
field.go Normal file
View File

@ -0,0 +1,158 @@
package zerolog
import (
"bytes"
"strconv"
"time"
)
var (
// TimestampFieldName is the field name used for the timestamp field.
TimestampFieldName = "time"
// LevelFieldName is the field name used for the level field.
LevelFieldName = "level"
// MessageFieldName is the field name used for the message field.
MessageFieldName = "message"
// ErrorFieldName is the field name used for error fields.
ErrorFieldName = "error"
// SampleFieldName is the name of the field used to report sampling.
SampleFieldName = "sample"
// TimeFieldFormat defines the time format of the Time field type.
TimeFieldFormat = time.RFC3339
now = time.Now
)
type FieldMode uint8
const (
zeroFieldMode FieldMode = iota
rawFieldMode
quotedFieldMode
precomputedFieldMode
timestampFieldMode
)
// field define a logger field.
type field struct {
key string
mode FieldMode
val string
json []byte
}
func (f field) writeJSON(buf *bytes.Buffer) {
switch f.mode {
case zeroFieldMode:
return
case precomputedFieldMode:
buf.Write(f.json)
return
case timestampFieldMode:
writeJSONString(buf, TimestampFieldName)
buf.WriteByte(':')
buf.WriteString(strconv.FormatInt(now().Unix(), 10))
return
}
writeJSONString(buf, f.key)
buf.WriteByte(':')
switch f.mode {
case quotedFieldMode:
writeJSONString(buf, f.val)
case rawFieldMode:
buf.WriteString(f.val)
default:
panic("unknown field mode")
}
}
func (f field) compileJSON() field {
switch f.mode {
case zeroFieldMode, precomputedFieldMode, timestampFieldMode:
return f
}
buf := &bytes.Buffer{}
f.writeJSON(buf)
cf := field{
mode: precomputedFieldMode,
json: buf.Bytes(),
}
return cf
}
func fStr(key, val string) field {
return field{key, quotedFieldMode, val, nil}
}
func fErr(err error) field {
return field{ErrorFieldName, quotedFieldMode, err.Error(), nil}
}
func fBool(key string, b bool) field {
if b {
return field{key, rawFieldMode, "true", nil}
}
return field{key, rawFieldMode, "false", nil}
}
func fInt(key string, i int) field {
return field{key, rawFieldMode, strconv.FormatInt(int64(i), 10), nil}
}
func fInt8(key string, i int8) field {
return field{key, rawFieldMode, strconv.FormatInt(int64(i), 10), nil}
}
func fInt16(key string, i int16) field {
return field{key, rawFieldMode, strconv.FormatInt(int64(i), 10), nil}
}
func fInt32(key string, i int32) field {
return field{key, rawFieldMode, strconv.FormatInt(int64(i), 10), nil}
}
func fInt64(key string, i int64) field {
return field{key, rawFieldMode, strconv.FormatInt(i, 10), nil}
}
func fUint(key string, i uint) field {
return field{key, rawFieldMode, strconv.FormatUint(uint64(i), 10), nil}
}
func fUint8(key string, i uint8) field {
return field{key, rawFieldMode, strconv.FormatUint(uint64(i), 10), nil}
}
func fUint16(key string, i uint16) field {
return field{key, rawFieldMode, strconv.FormatUint(uint64(i), 10), nil}
}
func fUint32(key string, i uint32) field {
return field{key, rawFieldMode, strconv.FormatUint(uint64(i), 10), nil}
}
func fUint64(key string, i uint64) field {
return field{key, rawFieldMode, strconv.FormatUint(i, 10), nil}
}
func fFloat32(key string, f float32) field {
return field{key, rawFieldMode, strconv.FormatFloat(float64(f), 'f', -1, 32), nil}
}
func fFloat64(key string, f float64) field {
return field{key, rawFieldMode, strconv.FormatFloat(f, 'f', -1, 64), nil}
}
func fTimestamp() field {
return field{mode: timestampFieldMode}
}
func fTime(key string, t time.Time) field {
return field{key, quotedFieldMode, t.Format(TimeFieldFormat), nil}
}

55
json.go Normal file
View File

@ -0,0 +1,55 @@
package zerolog
import (
"bytes"
"unicode/utf8"
)
const hex = "0123456789abcdef"
func writeJSONString(buf *bytes.Buffer, s string) {
buf.WriteByte('"')
for i := 0; i < len(s); {
if b := s[i]; b < utf8.RuneSelf {
switch b {
case '"', '\\':
buf.WriteByte('\\')
buf.WriteByte(b)
case '\b':
buf.WriteByte('\\')
buf.WriteByte('b')
case '\f':
buf.WriteByte('\\')
buf.WriteByte('f')
case '\n':
buf.WriteByte('\\')
buf.WriteByte('n')
case '\r':
buf.WriteByte('\\')
buf.WriteByte('r')
case '\t':
buf.WriteByte('\\')
buf.WriteByte('t')
default:
if b >= 0x20 {
buf.WriteByte(b)
} else {
buf.WriteString(`\u00`)
buf.WriteByte(hex[b>>4])
buf.WriteByte(hex[b&0xF])
}
}
i++
continue
}
r, size := utf8.DecodeRuneInString(s[i:])
if r == utf8.RuneError && size == 1 {
buf.WriteString(`\ufffd`)
i++
continue
}
buf.WriteString(s[i : i+size])
i += size
}
buf.WriteByte('"')
}

290
log.go Normal file
View File

@ -0,0 +1,290 @@
// Package zerolog provides a lightweight logging library dedicated to JSON logging.
//
// A global Logger can be use for simple logging:
//
// import "github.com/rs/zerolog/log"
//
// log.Info().Msg("hello world")
// // Output: {"time":1494567715,"level":"info","message":"hello world"}
//
// NOTE: To import the global logger, import the "log" subpackage "github.com/rs/zerolog/log".
//
// Fields can be added to log messages:
//
// log.Info().Str("foo", "bar").Msg("hello world")
// // Output: {"time":1494567715,"level":"info","message":"hello world","foo":"bar"}
//
// Create logger instance to manage different outputs:
//
// logger := zerolog.New(os.Stderr).With().Timestamp().Logger()
// logger.Info().
// Str("foo", "bar").
// Msg("hello world")
// // Output: {"time":1494567715,"level":"info","message":"hello world","foo":"bar"}
//
// Sub-loggers let you chain loggers with additional context:
//
// sublogger := log.With().Str("component": "foo").Logger()
// sublogger.Info().Msg("hello world")
// // Output: {"time":1494567715,"level":"info","message":"hello world","component":"foo"}
//
// Level logging
//
// zerolog.GlobalLevel = zerolog.InfoLevel
//
// log.Debug().Msg("filtered out message")
// log.Info().Msg("routed message")
//
// if e := log.Debug(); e.Enabled() {
// // Compute log output only if enabled.
// value := compute()
// e.Str("foo": value).Msg("some debug message")
// }
// // Output: {"level":"info","time":1494567715,"routed message"}
//
// Customize automatic field names:
//
// log.TimestampFieldName = "t"
// log.LevelFieldName = "p"
// log.MessageFieldName = "m"
//
// log.Info().Msg("hello world")
// // Output: {"t":1494567715,"p":"info","m":"hello world"}
//
// Log with no level and message:
//
// log.Log().Str("foo","bar").Msg("")
// // Output: {"time":1494567715,"foo":"bar"}
//
// Add contextual fields to global Logger:
//
// log.Logger = log.With().Str("foo", "bar").Logger()
//
// Sample logs:
//
// sampled := log.Sample(10)
// sampled.Info().Msg("will be logged every 10 messages")
//
package zerolog
import (
"io"
"os"
"sync/atomic"
)
type parentLogger interface {
addContextField(Event)
}
// Level defines log levels.
type Level uint8
const (
// DebugLevel defines debug log level.
DebugLevel Level = iota
// InfoLevel defines info log level.
InfoLevel
// WarnLevel defines warn log level.
WarnLevel
// ErrorLevel defines error log level.
ErrorLevel
// FatalLevel defines fatal log level.
FatalLevel
// PanicLevel defines panic log level.
PanicLevel
// Disabled disables the logger.
Disabled
)
func (l Level) String() string {
switch l {
case DebugLevel:
return "debug"
case InfoLevel:
return "info"
case WarnLevel:
return "warning"
case ErrorLevel:
return "error"
case FatalLevel:
return "fatal"
case PanicLevel:
return "panic"
}
return ""
}
const (
// Often samples log every 10 events.
Often = int64(10)
// Sometimes samples log every 100 events.
Sometimes = int64(100)
// Rarely samples log every 1000 events.
Rarely = int64(1000)
)
var (
// GlobalLevel defines the global override for log level. If this
// values is raised, all Loggers will use at least this value.
//
// To globally disable logs, set GlobalLevel to Disabled.
GlobalLevel = DebugLevel
// DisableSampling will disable sampling in all Loggers if true.
DisableSampling = false
)
// 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
// serialization to the Writer. If your Writer is not thread safe,
// you may consider a sync wrapper.
type Logger struct {
root bool
parent parentLogger
w LevelWriter
field field
level Level
sample uint32
counter *uint32
}
// New creates a root logger with given output writer.
func New(w io.Writer) Logger {
if w == nil {
panic("w is nil")
}
lw, ok := w.(LevelWriter)
if !ok {
lw = levelWriterAdapter{w}
}
return Logger{
root: true,
w: lw,
}
}
// With creates a child logger with the field added to its context.
func (l Logger) With() Context {
return Context{l}
}
// Level crestes a child logger with the minium accepted level set to level.
func (l Logger) Level(lvl Level) Logger {
return Logger{
parent: l,
w: l.w,
level: lvl,
sample: l.sample,
counter: l.counter,
}
}
// Sample returns a logger that only let one message out of every to pass thru.
func (l Logger) Sample(every int) Logger {
if every == 0 {
// Create a child with no sampling.
return Logger{
parent: l,
w: l.w,
level: l.level,
}
}
return Logger{
parent: l,
w: l.w,
level: l.level,
sample: uint32(every),
counter: new(uint32),
}
}
// Debug starts a new message with debug level.
//
// You must call Msg on the returned event in order to send the event.
func (l Logger) Debug() Event {
return l.newEvent(DebugLevel, true, nil)
}
// Info starts a new message with info level.
//
// You must call Msg on the returned event in order to send the event.
func (l Logger) Info() Event {
return l.newEvent(InfoLevel, true, nil)
}
// Warn starts a new message with warn level.
//
// You must call Msg on the returned event in order to send the event.
func (l Logger) Warn() Event {
return l.newEvent(WarnLevel, true, nil)
}
// Error starts a new message with error level.
//
// You must call Msg on the returned event in order to send the event.
func (l Logger) Error() Event {
return l.newEvent(ErrorLevel, true, nil)
}
// Fatal starts a new message with fatal level. The os.Exit(1) function
// is called by the Msg method.
//
// You must call Msg on the returned event in order to send the event.
func (l Logger) Fatal() Event {
return l.newEvent(FatalLevel, true, func(msg string) { os.Exit(1) })
}
// Panic starts a new message with panic level. The message is also sent
// to the panic function.
//
// You must call Msg on the returned event in order to send the event.
func (l Logger) Panic() Event {
return l.newEvent(PanicLevel, true, func(msg string) { panic(msg) })
}
// Log starts a new message with no level. Setting GlobalLevel to Disabled
// will still disable events produced by this method.
//
// You must call Msg on the returned event in order to send the event.
func (l Logger) Log() Event {
return l.newEvent(ErrorLevel, false, nil)
}
func (l Logger) newEvent(level Level, addLevelField bool, done func(string)) Event {
lvl := InfoLevel
if addLevelField {
lvl = level
}
e := newEvent(l.w, lvl, l.should(level))
if addLevelField {
e.Str(LevelFieldName, level.String())
}
if l.sample > 0 && SampleFieldName != "" {
e.Uint32(SampleFieldName, l.sample)
}
l.addContextField(e)
return e
}
// should returns true if the log event should be logged.
func (l Logger) should(lvl Level) bool {
if lvl < l.level || lvl < GlobalLevel {
return false
}
if !DisableSampling && l.sample > 0 && l.counter != nil {
c := atomic.AddUint32(l.counter, 1)
return c%l.sample == 0
}
return true
}
func (l Logger) addContextField(e Event) {
if !l.root {
l.parent.addContextField(e)
}
if l.field.mode != zeroFieldMode {
e.append(l.field)
}
}

78
log/log.go Normal file
View File

@ -0,0 +1,78 @@
// Package log provides a global logger for zerolog.
package log
import (
"os"
"github.com/rs/zerolog"
)
// Logger is the global logger.
var Logger = zerolog.New(os.Stderr).With().Timestamp().Logger()
// With creates a child logger with the field added to its context.
func With() zerolog.Context {
return Logger.With()
}
// Level crestes a child logger with the minium accepted level set to level.
func Level(level zerolog.Level) zerolog.Logger {
return Logger.Level(level)
}
// Sample returns a logger that only let one message out of every to pass thru.
func Sample(every int) zerolog.Logger {
return Logger.Sample(every)
}
// Debug starts a new message with debug level.
//
// You must call Msg on the returned event in order to send the event.
func Debug() zerolog.Event {
return Logger.Debug()
}
// Info starts a new message with info level.
//
// You must call Msg on the returned event in order to send the event.
func Info() zerolog.Event {
return Logger.Info()
}
// Warn starts a new message with warn level.
//
// You must call Msg on the returned event in order to send the event.
func Warn() zerolog.Event {
return Logger.Warn()
}
// Error starts a new message with error level.
//
// You must call Msg on the returned event in order to send the event.
func Error() zerolog.Event {
return Logger.Error()
}
// Fatal starts a new message with fatal level. The os.Exit(1) function
// is called by the Msg method.
//
// You must call Msg on the returned event in order to send the event.
func Fatal() zerolog.Event {
return Logger.Fatal()
}
// Panic starts a new message with panic level. The message is also sent
// to the panic function.
//
// You must call Msg on the returned event in order to send the event.
func Panic() zerolog.Event {
return Logger.Panic()
}
// Log starts a new message with no level. Setting zerolog.GlobalLevel to
// zerlog.Disabled will still disable events produced by this method.
//
// You must call Msg on the returned event in order to send the event.
func Log() zerolog.Event {
return Logger.Log()
}

101
log_example_test.go Normal file
View File

@ -0,0 +1,101 @@
package zerolog_test
import (
"errors"
"os"
"github.com/rs/zerolog"
)
func ExampleNew() {
log := zerolog.New(os.Stdout)
log.Info().Msg("hello world")
// Output: {"level":"info","message":"hello world"}
}
func ExampleLogger_With() {
log := zerolog.New(os.Stdout).
With().
Str("foo", "bar").
Logger()
log.Info().Msg("hello world")
// Output: {"level":"info","foo":"bar","message":"hello world"}
}
func ExampleLogger_Level() {
log := zerolog.New(os.Stdout).Level(zerolog.WarnLevel)
log.Info().Msg("filtered out message")
log.Error().Msg("kept message")
// Output: {"level":"error","message":"kept message"}
}
func ExampleLogger_Sample() {
log := zerolog.New(os.Stdout).Sample(2)
log.Info().Msg("message 1")
log.Info().Msg("message 2")
log.Info().Msg("message 3")
log.Info().Msg("message 4")
// Output: {"level":"info","sample":2,"message":"message 2"}
// {"level":"info","sample":2,"message":"message 4"}
}
func ExampleLogger_Debug() {
log := zerolog.New(os.Stdout)
log.Debug().
Str("foo", "bar").
Int("n", 123).
Msg("hello world")
// Output: {"level":"debug","foo":"bar","n":123,"message":"hello world"}
}
func ExampleLogger_Info() {
log := zerolog.New(os.Stdout)
log.Info().
Str("foo", "bar").
Int("n", 123).
Msg("hello world")
// Output: {"level":"info","foo":"bar","n":123,"message":"hello world"}
}
func ExampleLogger_Warn() {
log := zerolog.New(os.Stdout)
log.Warn().
Str("foo", "bar").
Msg("a warning message")
// Output: {"level":"warning","foo":"bar","message":"a warning message"}
}
func ExampleLogger_Error() {
log := zerolog.New(os.Stdout)
log.Error().
Err(errors.New("some error")).
Msg("error doing something")
// Output: {"level":"error","error":"some error","message":"error doing something"}
}
func ExampleLogger_Log() {
log := zerolog.New(os.Stdout)
log.Log().
Str("foo", "bar").
Str("bar", "baz").
Msg("")
// Output: {"foo":"bar","bar":"baz"}
}

157
log_test.go Normal file
View File

@ -0,0 +1,157 @@
package zerolog
import (
"bytes"
"reflect"
"testing"
)
func TestLog(t *testing.T) {
t.Run("empty", func(t *testing.T) {
out := &bytes.Buffer{}
log := New(out)
log.Log().Msg("")
if got, want := out.String(), "{}\n"; got != want {
t.Errorf("invalid log output: got %q, want %q", got, want)
}
})
t.Run("one-field", func(t *testing.T) {
out := &bytes.Buffer{}
log := New(out)
log.Log().Str("foo", "bar").Msg("")
if got, want := out.String(), `{"foo":"bar"}`+"\n"; got != want {
t.Errorf("invalid log output: got %q, want %q", got, want)
}
})
t.Run("two-field", func(t *testing.T) {
out := &bytes.Buffer{}
log := New(out)
log.Log().
Str("foo", "bar").
Int("n", 123).
Msg("")
if got, want := out.String(), `{"foo":"bar","n":123}`+"\n"; got != want {
t.Errorf("invalid log output: got %q, want %q", got, want)
}
})
}
func TestInfo(t *testing.T) {
t.Run("empty", func(t *testing.T) {
out := &bytes.Buffer{}
log := New(out)
log.Info().Msg("")
if got, want := out.String(), `{"level":"info"}`+"\n"; got != want {
t.Errorf("invalid log output: got %q, want %q", got, want)
}
})
t.Run("one-field", func(t *testing.T) {
out := &bytes.Buffer{}
log := New(out)
log.Info().Str("foo", "bar").Msg("")
if got, want := out.String(), `{"level":"info","foo":"bar"}`+"\n"; got != want {
t.Errorf("invalid log output: got %q, want %q", got, want)
}
})
t.Run("two-field", func(t *testing.T) {
out := &bytes.Buffer{}
log := New(out)
log.Info().
Str("foo", "bar").
Int("n", 123).
Msg("")
if got, want := out.String(), `{"level":"info","foo":"bar","n":123}`+"\n"; got != want {
t.Errorf("invalid log output: got %q, want %q", got, want)
}
})
}
func TestWith(t *testing.T) {
out := &bytes.Buffer{}
log := New(out).With().Str("f1", "val").Str("f2", "val").Logger()
log.Log().Str("f3", "val").Msg("")
if got, want := out.String(), `{"f1":"val","f2":"val","f3":"val"}`+"\n"; got != want {
t.Errorf("invalid log output: got %q, want %q", got, want)
}
}
func TestLevel(t *testing.T) {
t.Run("Disabled", func(t *testing.T) {
out := &bytes.Buffer{}
log := New(out).Level(Disabled)
log.Info().Msg("test")
if got, want := out.String(), ""; got != want {
t.Errorf("invalid log output: got %q, want %q", got, want)
}
})
t.Run("Info", func(t *testing.T) {
out := &bytes.Buffer{}
log := New(out).Level(InfoLevel)
log.Info().Msg("test")
if got, want := out.String(), `{"level":"info","message":"test"}`+"\n"; got != want {
t.Errorf("invalid log output: got %q, want %q", got, want)
}
})
}
func TestSampling(t *testing.T) {
out := &bytes.Buffer{}
log := New(out).Sample(2)
log.Log().Int("i", 1).Msg("")
log.Log().Int("i", 2).Msg("")
log.Log().Int("i", 3).Msg("")
log.Log().Int("i", 4).Msg("")
if got, want := out.String(), "{\"sample\":2,\"i\":2}\n{\"sample\":2,\"i\":4}\n"; got != want {
t.Errorf("invalid log output: got %q, want %q", got, want)
}
}
type levelWriter struct {
ops []struct {
l Level
p string
}
}
func (lw *levelWriter) Write(p []byte) (int, error) {
return len(p), nil
}
func (lw *levelWriter) WriteLevel(lvl Level, p []byte) (int, error) {
lw.ops = append(lw.ops, struct {
l Level
p string
}{lvl, string(p)})
return len(p), nil
}
func TestLevelWriter(t *testing.T) {
lw := &levelWriter{
ops: []struct {
l Level
p string
}{},
}
log := New(lw)
log.Debug().Msg("1")
log.Info().Msg("2")
log.Warn().Msg("3")
log.Error().Msg("4")
want := []struct {
l Level
p string
}{
{DebugLevel, `{"level":"debug","message":"1"}` + "\n"},
{InfoLevel, `{"level":"info","message":"2"}` + "\n"},
{WarnLevel, `{"level":"warning","message":"3"}` + "\n"},
{ErrorLevel, `{"level":"error","message":"4"}` + "\n"},
}
if got := lw.ops; !reflect.DeepEqual(got, want) {
t.Errorf("invalid ops:\ngot:\n%v\nwant:\n%v", got, want)
}
}

43
syslog.go Normal file
View File

@ -0,0 +1,43 @@
// +build !windows
package zerolog
import (
"log/syslog"
)
type syslogWriter struct {
w *syslog.Writer
}
// SyslogWriter wraps a syslog.Writer and set the right syslog level
// matching the log even level.
func SyslogWriter(w *syslog.Writer) LevelWriter {
return syslogWriter{w}
}
func (sw syslogWriter) Write(p []byte) (n int, err error) {
return sw.w.Write(p)
}
// WriteLevel implements LevelWriter interface.
func (sw syslogWriter) WriteLevel(level Level, p []byte) (n int, err error) {
switch level {
case DebugLevel:
err = sw.w.Debug(string(p))
case InfoLevel:
err = sw.w.Info(string(p))
case WarnLevel:
err = sw.w.Warning(string(p))
case ErrorLevel:
err = sw.w.Err(string(p))
case FatalLevel:
err = sw.w.Emerg(string(p))
case PanicLevel:
err = sw.w.Crit(string(p))
default:
panic("invalid level")
}
n = len(p)
return
}

65
writer.go Normal file
View File

@ -0,0 +1,65 @@
package zerolog
import "io"
// LevelWriter defines as interface a writer may implement in order
// to receive level information with payload.
type LevelWriter interface {
io.Writer
WriteLevel(level Level, p []byte) (n int, err error)
}
type levelWriterAdapter struct {
io.Writer
}
func (lw levelWriterAdapter) WriteLevel(level Level, p []byte) (n int, err error) {
return lw.Write(p)
}
type multiLevelWriter struct {
writers []LevelWriter
}
func (t multiLevelWriter) Write(p []byte) (n int, err error) {
for _, w := range t.writers {
n, err = w.Write(p)
if err != nil {
return
}
if n != len(p) {
err = io.ErrShortWrite
return
}
}
return len(p), nil
}
func (t multiLevelWriter) WriteLevel(l Level, p []byte) (n int, err error) {
for _, w := range t.writers {
n, err = w.WriteLevel(l, p)
if err != nil {
return
}
if n != len(p) {
err = io.ErrShortWrite
return
}
}
return len(p), nil
}
// MultiLevelWriter creates a writer that duplicates its writes to all the
// provided writers, similar to the Unix tee(1) command. If some writers
// implement LevelWriter, their WriteLevel method will be used instead of Write.
func MultiLevelWriter(writers ...io.Writer) LevelWriter {
lwriters := make([]LevelWriter, 0, len(writers))
for _, w := range writers {
if lw, ok := w.(LevelWriter); ok {
lwriters = append(lwriters, lw)
} else {
lwriters = append(lwriters, levelWriterAdapter{w})
}
}
return multiLevelWriter{lwriters}
}