Zap
Blazing fast, structured, leveled logging in Go.
示例代码参见zap
1. Quick Start
1.1 Installation
go get -u go.uber.org/zap
1.2 Sugared Logger
In contexts where performance is nice, but not critical, use the SugaredLogger
.
It’s 4-10x faster than other structured logging packages and includes both structured and printf
-style APIs.
func main() {
logger, err := zap.NewDevelopment()
if err != nil {
log.Fatalf("can't initialize zap logger: %v", err)
}
defer logger.Sync()
sugar := logger.Sugar()
url := "Example URL"
sugar.Infow("failed to fetch URL",
"url", url,
"attempt", 3,
"backoff", time.Second,
)
sugar.Infof("Failed to fetch URL %s", "url")
}
logger.Sync()
: flushes buffer, if anysugar.Infow()
: Structured context as loosely typed key-value pairs
1.3 Logger
When performance and type safety are critical, use the Logger
. It’s even faster than the SugaredLogger
and allocates far less, but it only supported structured logging
func main() {
logger, err := zap.NewDevelopment()
if err != nil {
log.Fatalf("can't initialize zap logger: %v", err)
}
defer logger.Sync()
url := "example url"
logger.Info("failed to fetch URL",
zap.String("url", url),
zap.Int("attempt", 3),
zap.Duration("backoff", time.Second))
}
2. Overview
Package zap provides fast, structured, leveled logging.
For application that log in the hot path, reflection-based serialization and string formatting are prohibitively expensive - they are CPU-intensive and make many small allocations. Put differently, using json.Marhshal and fmt.Fprintf to log tons of interface{} makes your application slow.
Zap takes a different approach. It includes a reflection-free, zero-allocation JSON encoder, and the base Logger strives to avoid serialization overhead and allocations wherever possible. By building the high-level SugaredLogger on that foundation, zap let users choose when they need to count every allocation and when they’d prefer a more familiar, loosely typed API.
2.1 Choosing a Logger
In contexts where performance is nice, but not critical, use the SugaredLogger. It’s 4-10x faster than other structured logging packages and supports both structured and printf-style loggin. Like log15 and go-kit, the SugaredLogger’s structured logging APIs are loosely typed and accept a variadic number of key-value paires.
sugar := zap.NewExample().Sugar()
defer sugar.Sync()
sugar.Infow("failed to fetch URL",
"url", "http://example.com",
"attempt", 3,
"backoff", time.Second,
)
sugar.Infof("failed to fetch URL: %s", "http://example.com")
By default, loggers are unbuffered. However, since zap’s low-level APIs allow buffering, calling Sync before letting your process exit is a good habit.
In the rare contexts where every microsecond and every allocation matter, use the Logger. It’s even faster than the SugaredLogger and allocates far less, but it only supports strongly-typed, structured logging.
logger := zap.NewExample()
defer logger.Sync()
logger.Info("failed to fetch URL",
zap.String("url", "http://example.com"),
zap.Int("attempt", 3),
zap.Duration("backoff", time.Second),
)
Choosing between the Logger and SugaredLogger doesn’t need to an application-wide decision: converting between the two is simple and inexpensive.
logger := zap.NewExample()
defer logger.Sync()
suger := logger.Sugar()
plain := sugar.Desugar()
2.2 Configuring Zap
The simplest way to build a Logger is to use zap’s opinionated presets: NewExample, NewProduction, and NewDevelopment. These presets build a logger with a single function call:
logger, err := zap.NewProduction()
if err != nil {
log.Fatalf("can't initialize zap logger: %v", err)
}
defer logger.Sync()
Presets are fine for small projects, but larger projects and organizations naturally require a bit more customization. For most users, zap’s Config struct strikes the right balance between flexibility and convenience.
More unusual configurations ( splitting output between files, sending logs to a message queue, etc.) are possible, but require direct use of go.uber.org/zap/zapcore.
2.3 Extending Zap
The zap package itself is a relatively thin wrapper around the interface in go.uber.org/zap/zapcore. Extending zap to support a new encoding ( e.g. BSON), a new log sink ( e.g. Kafka), or something more exotic (perhaps an exception aggregation service, like Sentry or Rollbar ) typically requires implementing the zapcore.Encoder, zapcore.WriteSyncer, or zapcore.Core interfaces.
Similarly, package authors can use the high-performance Encoder and Core implementations in the zapcore package to build their own loggers.
3. Examples
3.1 Presets
package main
import (
"time"
"go.uber.org/zap"
)
func main() {
// Using zap's preset constructors is the simplest way to get a feel for the
// package, but they don't allow much customization.
logger := zap.NewExample() // or NewProduction, or NewDevelopment
defer logger.Sync()
const url = "http://example.com"
// In most circumstances, use the SugaredLogger. It's 4-10x faster than most
// other structured logging packages and has a familiar, loosely-typed API.
sugar := logger.Sugar()
sugar.Infow("Failed to fetch URL.",
// Structured context as loosely typed key-value pairs.
"url", url,
"attempt", 3,
"backoff", time.Second,
)
sugar.Infof("Failed to fetch URL: %s", url)
// In the unusual situations where every microsecond matters, use the
// Logger. It's even faster than the SugaredLogger, but only supports
// structured logging.
logger.Info("Failed to fetch URL.",
// Structured context as strongly typed fields.
zap.String("url", url),
zap.Int("attempt", 3),
zap.Duration("backoff", time.Second),
)
}
{"level":"info","msg":"Failed to fetch URL.","url":"http://example.com","attempt":3,"backoff":"1s"}
{"level":"info","msg":"Failed to fetch URL: http://example.com"}
{"level":"info","msg":"Failed to fetch URL.","url":"http://example.com","attempt":3,"backoff":"1s"}
三种预设的区别:
NewDevelopment
: builds a development Logger that writes DebugLevel and above logs to standard error in a human-friendly format.It's a shortcut for NewDevelopmentConfig().Build(...Option).
NewExample
: builds a Logger that's designed for use in zap's testable examples. It writes DebugLevel and above logs to standard out as JSON, but omits the timestamp and calling function to keep example output short and deterministicNewProduction
: builds a sensible production Logger that writes InfoLevel and above logs to standard error as JSON.It's a shortcut for NewProductionConfig().Build(...Option).
func main() {
url := "example"
devLogger, _ := zap.NewDevelopment()
defer devLogger.Sync()
devLogger.Info("failed to fetch url",
zap.String("url", url),
zap.Int("attempt", 3),
zap.Duration("backoff", time.Second))
exampleLogger := zap.NewExample()
defer exampleLogger.Sync()
exampleLogger.Info("failed to fetch url",
zap.String("url", url),
zap.Int("attempt", 3),
zap.Duration("backoff", time.Second))
prodLogger, _ := zap.NewProduction()
defer prodLogger.Sync()
prodLogger.Info("failed to fetch url",
zap.String("url", url),
zap.Int("attempt", 3),
zap.Duration("backoff", time.Second))
}
2021-12-20T11:15:52.398+0800 INFO presets/main.go:13 failed to fetch url {"url": "example", "attempt": 3, "backoff": "1s"}
{"level":"info","msg":"failed to fetch url","url":"example","attempt":3,"backoff":"1s"}
{"level":"info","ts":1639970152.3984973,"caller":"presets/main.go:27","msg":"failed to fetch url","url":"example","attempt":3,"backoff":1}
3.2 Basic Configuration
package main
import (
"encoding/json"
"go.uber.org/zap"
)
func main() {
// For some users, the presets offered by the NewProduction, NewDevelopment,
// and NewExample constructors won't be appropriate. For most of those
// users, the bundled Config struct offers the right balance of flexibility
// and convenience. (For more complex needs, see the AdvancedConfiguration
// example.)
//
// See the documentation for Config and zapcore.EncoderConfig for all the
// available options.
rawJSON := []byte(`{
"level": "debug",
"encoding": "json",
"outputPaths": ["stdout", "/tmp/logs"],
"errorOutputPaths": ["stderr"],
"initialFields": {"foo": "bar"},
"encoderConfig": {
"messageKey": "message",
"levelKey": "level",
"levelEncoder": "lowercase"
}
}`)
var cfg zap.Config
if err := json.Unmarshal(rawJSON, &cfg); err != nil {
panic(err)
}
logger, err := cfg.Build()
if err != nil {
panic(err)
}
defer logger.Sync()
logger.Info("logger construction succeeded")
}
3.3 Advanced Configuration
package main
import (
"io/ioutil"
"os"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func main() {
// The bundled Config struct only supports the most common configuration
// options. More complex needs, like splitting logs between multiple files
// or writing to non-file outputs, require use of the zapcore package.
//
// In this example, imagine we're both sending our logs to Kafka and writing
// them to the console. We'd like to encode the console output and the Kafka
// topics differently, and we'd also like special treatment for
// high-priority logs.
// First, define our level-handling logic.
highPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
return lvl >= zapcore.ErrorLevel
})
lowPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
return lvl < zapcore.ErrorLevel
})
// Assume that we have clients for two Kafka topics. The clients implement
// zapcore.WriteSyncer and are safe for concurrent use. (If they only
// implement io.Writer, we can use zapcore.AddSync to add a no-op Sync
// method. If they're not safe for concurrent use, we can add a protecting
// mutex with zapcore.Lock.)
topicDebugging := zapcore.AddSync(ioutil.Discard)
topicErrors := zapcore.AddSync(ioutil.Discard)
// High-priority output should also go to standard error, and low-priority
// output should also go to standard out.
consoleDebugging := zapcore.Lock(os.Stdout)
consoleErrors := zapcore.Lock(os.Stderr)
// Optimize the Kafka output for machine consumption and the console output
// for human operators.
kafkaEncoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())
// Join the outputs, encoders, and level-handling functions into
// zapcore.Cores, then tee the four cores together.
core := zapcore.NewTee(
zapcore.NewCore(kafkaEncoder, topicErrors, highPriority),
zapcore.NewCore(consoleEncoder, consoleErrors, highPriority),
zapcore.NewCore(kafkaEncoder, topicDebugging, lowPriority),
zapcore.NewCore(consoleEncoder, consoleDebugging, lowPriority),
)
// From a zapcore.Core, it's easy to construct a Logger.
logger := zap.New(core)
defer logger.Sync()
logger.Info("constructed a logger")
}
4. Log rotation
Zap doesn’t natively support rotating log files. It’s easy to integrate a log rotation package like gopkg.in/natefinch/lumberjack.v2
as a zapcore.WriteSyncer
go get -u gopkg.in/natefinch/lumberjack.v2
const num = 10000
func main() {
// lumberjack.Logger is already safe for concurrent use, so we don't need to
// lock it.
w := zapcore.AddSync(&lumberjack.Logger{
Filename: "./logs/foo.log",
MaxSize: 1, // megabytes
MaxBackups: 3,
MaxAge: 28, // days
})
consoleSyncer := zapcore.Lock(os.Stdout)
fileEncoder := zapcore.NewJSONEncoder(zap.NewDevelopmentEncoderConfig())
consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())
//syncer := zapcore.NewMultiWriteSyncer(w)
//core := zapcore.NewCore(
// zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()),
// syncer,
// zap.InfoLevel,
//)
core := zapcore.NewTee(
zapcore.NewCore(fileEncoder, w, zap.InfoLevel),
zapcore.NewCore(consoleEncoder, consoleSyncer, zap.InfoLevel),
)
logger := zap.New(core)
for i := 0; i < num; i++ {
logger.Info("log rotation: ", zap.Int("No", i))
}
}
If MaxBackups and MaxAge are both 0, no old log files will be deleted.
Reference
- zap docs
- FAQ zap FAQ
- lumberjack github repo