commit 7f302b00ec7268d6b4bcb028c2409152ab4fe0b9 Author: Olivier Poitrey Date: Thu May 11 22:24:39 2017 -0700 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..daf913b --- /dev/null +++ b/.gitignore @@ -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 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..96da367 --- /dev/null +++ b/.travis.yml @@ -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 ./... diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..677e07f --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0eadb36 --- /dev/null +++ b/README.md @@ -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. + diff --git a/benchmark_test.go b/benchmark_test.go new file mode 100644 index 0000000..926f0d4 --- /dev/null +++ b/benchmark_test.go @@ -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) + } + }) +} diff --git a/context.go b/context.go new file mode 100644 index 0000000..a44d42b --- /dev/null +++ b/context.go @@ -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)) +} diff --git a/event.go b/event.go new file mode 100644 index 0000000..4cd06c6 --- /dev/null +++ b/event.go @@ -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)) +} diff --git a/field.go b/field.go new file mode 100644 index 0000000..4aca84b --- /dev/null +++ b/field.go @@ -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} + +} diff --git a/json.go b/json.go new file mode 100644 index 0000000..1d1d628 --- /dev/null +++ b/json.go @@ -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('"') +} diff --git a/log.go b/log.go new file mode 100644 index 0000000..0ad0297 --- /dev/null +++ b/log.go @@ -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) + } +} diff --git a/log/log.go b/log/log.go new file mode 100644 index 0000000..86dd032 --- /dev/null +++ b/log/log.go @@ -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() +} diff --git a/log_example_test.go b/log_example_test.go new file mode 100644 index 0000000..27e19f3 --- /dev/null +++ b/log_example_test.go @@ -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"} +} diff --git a/log_test.go b/log_test.go new file mode 100644 index 0000000..b51b910 --- /dev/null +++ b/log_test.go @@ -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) + } +} diff --git a/syslog.go b/syslog.go new file mode 100644 index 0000000..23ec6fa --- /dev/null +++ b/syslog.go @@ -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 +} diff --git a/writer.go b/writer.go new file mode 100644 index 0000000..10f133c --- /dev/null +++ b/writer.go @@ -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} +}