Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add convenience method for ECS conform config. #12

Merged
merged 7 commits into from
Apr 24, 2020
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# CHANGELOG
Changelog for ecszap

## 0.2.0 (unreleased)

### Enhancement
* Add `ecszap.ECSCompatibleEncoderConfig` for making existing encoder config ECS conformant [pull#12](https://github.com/elastic/ecs-logging-go-zap/pull/12)
* Add method `ToZapCoreEncoderConfig` to `ecszap.EncoderConfig` for advanced use cases [pull#12](https://github.com/elastic/ecs-logging-go-zap/pull/12)

### Bug Fixes
* Use `zapcore.ISO8601TimeEncoder` as default instead of `ecszap.EpochMicrosTimeEncoder` [pull#12](https://github.com/elastic/ecs-logging-go-zap/pull/12)

### Breaking Change
* remove `ecszap.NewJSONEncoder` [pull#12](https://github.com/elastic/ecs-logging-go-zap/pull/12)

## 0.1.0
Initial Pre-Release supporting [MVP](https://github.com/elastic/ecs-logging/tree/master/spec#minimum-viable-product) for ECS conformant logging
103 changes: 46 additions & 57 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,34 +34,25 @@ require go.elastic.co/ecszap master
```

## Example usage
### Set up default logger
```
simitt marked this conversation as resolved.
Show resolved Hide resolved
encoderConfig := ecszap.NewDefaultEncoderConfig()
core := ecszap.NewCore(encoderConfig, os.Stdout, zap.DebugLevel)
logger := zap.New(core, zap.AddCaller())
```
import (
"errors"
"os"

pkgerrors "github.com/pkg/errors"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"

"go.elastic.co/ecszap"
)
### Use structured logging
```
// Adding fields and a logger name
logger = logger.With(zap.String("custom", "foo"))
logger = logger.Named("mylogger")

// Use strongly typed Field values
logger.Info("some logging info",
zap.Int("count", 17),
zap.Error(errors.New("boom")),
}

func main() {
// Create logger using an ecszap.Core instance
cfg := ecszap.NewDefaultEncoderConfig()
core := ecszap.NewCore(cfg, os.Stdout, zap.DebugLevel)
logger := zap.New(core, zap.AddCaller())
defer logger.Sync()

// Adding fields and a logger name
logger = logger.With(zap.String("custom", "foo"))
logger = logger.Named("mylogger")

// Use strongly typed Field values
logger.Info("some logging info",
zap.Int("count", 17),
zap.Error(errors.New("boom")),
)
// Log Output:
//{
// "log.level":"info",
Expand All @@ -79,10 +70,13 @@ func main() {
// "message":"boom"
// }
//}
```

### Log Errors
simitt marked this conversation as resolved.
Show resolved Hide resolved
```
err := errors.New("boom")
logger.Error("some error", zap.Error(pkgerrors.Wrap(err, "crash")))

// Log a wrapped error
err := errors.New("boom")
logger.Error("some error", zap.Error(pkgerrors.Wrap(err, "crash")))
// Log Output:
//{
// "log.level":"error",
Expand All @@ -100,13 +94,16 @@ func main() {
// "stacktrace": "\nexample.example\n\t/Users/xyz/example/example.go:50\nruntime.example\n\t/Users/xyz/.gvm/versions/go1.13.8.darwin.amd64/src/runtime/proc.go:203\nruntime.goexit\n\t/Users/xyz/.gvm/versions/go1.13.8.darwin.amd64/src/runtime/asm_amd64.s:1357"
// }
//}
```

### Use sugar logger
```
sugar := logger.Sugar()
sugar.Infow("some logging info",
"foo", "bar",
"count", 17,
)

// Use sugar logger with key-value pairs
sugar := logger.Sugar()
sugar.Infow("some logging info",
"foo", "bar",
"count", 17,
)
// Log Output:
//{
// "log.level":"info",
Expand All @@ -122,32 +119,24 @@ func main() {
// "foo":"bar",
// "count":17
//}
```

// Advanced use case: wrap a custom core with ecszap core
// create your own non-ECS core using a ecszap JSONEncoder
encoder := ecszap.NewJSONEncoder(ecszap.NewDefaultEncoderConfig())
core = zapcore.NewCore(encoder, os.Stdout, zap.DebugLevel)
// wrap your own core with the ecszap core
logger = zap.New(ecszap.WrapCore(core), zap.AddCaller())
defer logger.Sync()
logger.With(zap.Error(errors.New("wrapCore"))).Error("boom")
// Log Output:
//{
// "log.level":"error",
// "@timestamp":1584716847524082,
// "log.origin":{
// "file.name":"main/main.go",
// "file.line":338
// },
// "message":"boom",
// "ecs.version":"1.5.0",
// "error":{
// "message":"wrapCore"
// }
//}
}
### Advanced Use Case
simitt marked this conversation as resolved.
Show resolved Hide resolved
```
encoderConfig := ecszap.NewDefaultEncoderConfig()
encoder := zapcore.NewJSONEncoder(encoderConfig.ToZapCoreEncoderConfig())
syslogCore := newSyslogCore(encoder, level) //create your own loggers
core := ecszap.WrapCore(syslogCore)
logger := zap.New(core, zap.AddCaller())
```

### Transition from existing configurations
```
encoderConfig := ecszap.ECSCompatibleEncoderConfig(zap.NewDevelopmentEncoderConfig())
encoder := zapcore.NewJSONEncoder(encoderConfig)
core := zapcore.NewCore(encoder, os.Stdout, zap.DebugLevel)
logger := zap.New(ecszap.WrapCore(core), zap.AddCaller())
```

## References
* Introduction to ECS [blog post](https://www.elastic.co/blog/introducing-the-elastic-common-schema).
Expand Down
13 changes: 11 additions & 2 deletions core.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,25 @@
package ecszap

import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"

"go.elastic.co/ecszap/internal"
)

const version = "1.5.0"

// NewCore creates a zapcore.Core that uses an ECS conformant JSON encoder.
// This is the safest way to create an ECS compatible core.
func NewCore(cfg EncoderConfig, ws zapcore.WriteSyncer, enab zapcore.LevelEnabler) zapcore.Core {
return WrapCore(zapcore.NewCore(NewJSONEncoder(cfg), ws, enab))
enc := zapcore.NewJSONEncoder(cfg.ToZapCoreEncoderConfig())
return WrapCore(zapcore.NewCore(enc, ws, enab))
}

// WrapCore wraps a given core with the ecszap.core functionality
// WrapCore wraps a given core with ECS core functionality. For ECS
// compatibility, ensure that the wrapped zapcore.Core uses an encoder
// that is created from an ECS compatible configuration. For further details
// check out ecszap.EncoderConfig or ecszap.ECSCompatibleEncoderConfig.
func WrapCore(c zapcore.Core) zapcore.Core {
return &core{c}
}
Expand Down Expand Up @@ -58,6 +66,7 @@ func (c core) Check(ent zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.Checke
// before serializing the entry and fields.
func (c core) Write(ent zapcore.Entry, fields []zapcore.Field) error {
convertToECSFields(fields)
fields = append(fields, zap.String("ecs.version", version))
return c.Core.Write(ent, fields)
}

Expand Down
130 changes: 83 additions & 47 deletions json_encoder.go → encoder_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,25 @@ import (
"go.uber.org/zap/zapcore"
)

const version = "1.5.0"

var (
defaultLineEnding = zapcore.DefaultLineEnding
defaultEncodeName = zapcore.FullNameEncoder
defaultEncodeLevel = zapcore.LowercaseLevelEncoder
defaultEncodeDuration = zapcore.NanosDurationEncoder
defaultEncodeLevel = zapcore.LowercaseLevelEncoder
defaultEncodeCaller = ShortCallerEncoder
defaultEncodeTime = zapcore.ISO8601TimeEncoder

callerKey = "log.origin"
logLevelKey = "log.level"
logNameKey = "log.logger"
messageKey = "message"
stacktraceKey = "log.origin.stacktrace"
timeKey = "@timestamp"
)

// EncoderConfig allows customization of None-ECS settings.
// EncoderConfig exports all non ECS related configurable settings.
// The configuration can be used to create an ECS compatible zapcore.Core
type EncoderConfig struct {

// EnableName controls if a logger's name should be serialized
// when available. If enabled, the EncodeName configuration is
// used for serialization.
Expand Down Expand Up @@ -62,9 +68,12 @@ type EncoderConfig struct {
// EncodeCaller defines how an entry caller should be serialized.
// It will only be applied if EnableCaller is set to true.
EncodeCaller CallerEncoder `json:"callerEncoder" yaml:"callerEncoder"`

// EncodeTime defines how the log timestamp should be serialized
EncodeTime zapcore.TimeEncoder `json:"timeEncoder" yaml:"timeEncoder"`
}

// NewDefaultEncoderConfig returns EncoderConfig with default settings.
// NewDefaultEncoderConfig returns an EncoderConfig with default settings.
func NewDefaultEncoderConfig() EncoderConfig {
return EncoderConfig{
EnableName: true,
Expand All @@ -78,57 +87,84 @@ func NewDefaultEncoderConfig() EncoderConfig {
}
}

func (ec EncoderConfig) convertToZapCoreEncoderConfig() zapcore.EncoderConfig {
cfg := zapcore.EncoderConfig{
MessageKey: "message",
LevelKey: "log.level",
TimeKey: "@timestamp",
LineEnding: ec.LineEnding,
EncodeTime: EpochMicrosTimeEncoder,
EncodeDuration: ec.EncodeDuration,
EncodeName: ec.EncodeName,
EncodeCaller: zapcore.CallerEncoder(ec.EncodeCaller),
EncodeLevel: ec.EncodeLevel,
// ToZapCoreEncoderConfig transforms the ecszap.EncoderConfig into
// a zapcore.EncoderConfig
func (cfg EncoderConfig) ToZapCoreEncoderConfig() zapcore.EncoderConfig {
encCfg := zapcore.EncoderConfig{
MessageKey: messageKey,
LevelKey: logLevelKey,
TimeKey: timeKey,
EncodeTime: cfg.EncodeTime,
LineEnding: cfg.LineEnding,
EncodeDuration: cfg.EncodeDuration,
EncodeName: cfg.EncodeName,
EncodeLevel: cfg.EncodeLevel,
}
if cfg.EncodeDuration == nil {
ec.EncodeDuration = defaultEncodeDuration
if encCfg.EncodeTime == nil {
encCfg.EncodeTime = defaultEncodeTime
}
if ec.EnableName {
cfg.NameKey = "log.logger"
if cfg.EncodeName == nil {
ec.EncodeName = defaultEncodeName
if encCfg.EncodeDuration == nil {
encCfg.EncodeDuration = defaultEncodeDuration
}
if cfg.EnableName {
encCfg.NameKey = logNameKey
if encCfg.EncodeName == nil {
encCfg.EncodeName = defaultEncodeName
}
}
if ec.EnableStacktrace {
cfg.StacktraceKey = "log.origin.stacktrace"
if cfg.EnableStacktrace {
encCfg.StacktraceKey = stacktraceKey
}
if ec.EnableCaller {
cfg.CallerKey = "log.origin"
if cfg.EnableCaller {
encCfg.CallerKey = callerKey
if cfg.EncodeCaller == nil {
cfg.EncodeCaller = defaultEncodeCaller
encCfg.EncodeCaller = defaultEncodeCaller
} else {
encCfg.EncodeCaller = zapcore.CallerEncoder(cfg.EncodeCaller)
}
}
if encCfg.EncodeLevel == nil {
encCfg.EncodeLevel = defaultEncodeLevel
}
return encCfg
}

// ECSCompatibleEncoderConfig takes an existing zapcore.EncoderConfig
// and sets ECS relevant configuration options to ECS conformant values.
// The returned zapcore.EncoderConfig can be used to create
// an ECS conformant encoder.
// Be aware that this will always replace any set EncodeCaller function
// with the ecszap.ShortCallerEncoder.
// This is a pure convenience function for making a transition from
// existing an zap logger to an ECS conformant zap loggers easier.
// It is recommended to make use of the ecszap.EncoderConfig whenever possible.
func ECSCompatibleEncoderConfig(cfg zapcore.EncoderConfig) zapcore.EncoderConfig {
// set the required MVP ECS keys
cfg.MessageKey = messageKey
cfg.LevelKey = logLevelKey
cfg.TimeKey = timeKey
if cfg.NameKey != "" {
cfg.NameKey = logNameKey
}
// set further ECS defined keys only if keys were defined,
// as zap omits these log attributes when keys are not defined
// and ecszap does not intend to change this logic
if cfg.StacktraceKey != "" {
cfg.StacktraceKey = stacktraceKey
}
if cfg.CallerKey != "" {
cfg.CallerKey = callerKey
cfg.EncodeCaller = defaultEncodeCaller
}
// ensure all required encoders are set
if cfg.EncodeTime == nil {
cfg.EncodeTime = defaultEncodeTime
}
if cfg.EncodeDuration == nil {
cfg.EncodeDuration = defaultEncodeDuration
}
if cfg.EncodeLevel == nil {
cfg.EncodeLevel = defaultEncodeLevel
}
return cfg
}

type jsonEncoder struct {
zapcore.Encoder
}

// NewJSONEncoder creates a JSON encoder, populating a minimal
// set of Elastic common schema (ECS) fields.
func NewJSONEncoder(cfg EncoderConfig) zapcore.Encoder {
enc := jsonEncoder{zapcore.NewJSONEncoder(cfg.convertToZapCoreEncoderConfig())}
enc.AddString("ecs.version", version)
return &enc
}

// Clone wraps the zap.JSONEncoder Clone() method.
func (enc *jsonEncoder) Clone() zapcore.Encoder {
clone := jsonEncoder{}
clone.Encoder = enc.Encoder.Clone()
return &clone
}