diff --git a/.circleci/config.yml b/.circleci/config.yml index 9809e2e..0ace34a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -6,7 +6,7 @@ orbs: jobs: build: docker: - - image: circleci/golang:1.13 + - image: circleci/golang:1.14 steps: - checkout - run: go mod vendor @@ -16,8 +16,8 @@ jobs: mkdir -p /tmp/artifacts - run: command: | - go test -coverprofile=c.out - go tool cover -html=c.out -o coverage.html + make lint + make test mv coverage.html /tmp/artifacts mv c.out /tmp/artifacts - store_artifacts: diff --git a/.gitignore b/.gitignore index f483ae5..47f00e2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ .idea vendor c.out -coverage.html \ No newline at end of file +coverage.html +.golangci.yml +golangci-lint \ No newline at end of file diff --git a/LICENSE b/LICENSE index c7adde9..7a112ed 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 Spacetab.io +Copyright (c) 2021 spacetab.io Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile index 70ee257..8d8c159 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,29 @@ # ---- -## Test stuff +## LINTER stuff start +LINTER_VERSION=v1.27.0 + +get_lint_binary: + @[ -f ./golangci-lint ] && echo "golangci-lint exists" || ( echo "getting golangci-lint" && curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b ./ $(LINTER_VERSION) && ./golangci-lint --version ) +.PHONY: get_lint_binary get_lint_config: - @[ -f ./.golangci.yml ] && echo ".golangci.yml exists" || ( echo "getting .golangci.yml" && curl -O https://raw.githubusercontent.com/microparts/docker-golang/master/lint/.golangci.yml ) + @[ -f .golangci.yml ] && echo ".golangci.yml exists" || ( echo "getting .golangci.yml" && curl -O https://raw.githubusercontent.com/microparts/docker-golang/master/.golangci.yml ) .PHONY: get_lint_config -lint: get_lint_config - golangci-lint run ./... -v +lint: get_lint_binary get_lint_config + ./golangci-lint run -v .PHONY: lint +lint_quiet: get_lint_binary get_lint_config + @./golangci-lint run +.PHONY: lint_quiet + +## LINTER stuff end +# ---- + +# ---- +## TEST stuff start + test-unit: go test $$(go list ./...) --race --cover -count=1 -timeout 1s -coverprofile=c.out -v .PHONY: test-unit @@ -20,4 +35,8 @@ coverage-html: test: test-unit coverage-html .PHONY: test -# ---- \ No newline at end of file +## TEST stuff end +# ---- + +circle: + circleci local execute diff --git a/README.md b/README.md index 792829f..33ef12e 100644 --- a/README.md +++ b/README.md @@ -3,37 +3,35 @@ logs-go [![CircleCI](https://circleci.com/gh/spacetab-io/logs-go.svg?style=shield)](https://circleci.com/gh/spacetab-io/logs-go) [![codecov](https://codecov.io/gh/spacetab-io/logs-go/graph/badge.svg)](https://codecov.io/gh/spacetab-io/logs-go) -[Logrus](github.com/sirupsen/logrus) wrapper for easy use with sentry hook, database (gorm) and mux (gin) loggers. +Wrapper for [zerolog](https://github.com/rs/zerolog) tuned to work with [configuration](https://github.com/spacetab-io/configuration-go) and sentry hook. ## Usage -Initiate new logger with filled `logs.Config` and use it as common logrus logger instance +Initiate new logger with filled `log.Config` and use it as common zerolog ```go package main import ( - "time" - - "github.com/spacetab-io/logs-go" + "github.com/spacetab-io/logs-go/v2" ) func main() { - conf := &logs.Config{ - LogLevel: "warn", - Debug: true, - Sentry: &logs.SentryConfig{ + conf := log.Config{ + Level: "warn", + Format: "text", + ShowCaller: true, + Sentry: &log.SentryConfig{ Enable: true, - DSN: "http://dsn.sentry.com", + DSN: "http://dsn.sentry.com", }, } - - l, err := logs.NewLogger(conf) - if err != nil { + + if err := log.Init("test", conf, "logs-go", "v2.*.*", nil); err != nil { panic(err) } - - l.Warn("log some warning") + + log.Warn().Msg("log some warning") } ``` diff --git a/config.go b/config.go index b68589b..37bdd9b 100644 --- a/config.go +++ b/config.go @@ -1,28 +1,21 @@ -package logs +package log -import "os" +type Format string + +const ( + FormatText = "text" + FormatJSON = "json" +) type Config struct { - Stage string - LogLevel string `yaml:"level"` - Debug bool `yaml:"debug"` - Sentry *SentryConfig `yaml:"sentry"` + Level string `yaml:"level"` + Format Format `yaml:"format"` + NoColor bool `yaml:"no_color"` + ShowCaller bool `yaml:"show_caller"` + Sentry *SentryConfig `yaml:"sentry,omitempty"` } type SentryConfig struct { - Enable bool `yaml:"enable"` DSN string `yaml:"dsn"` -} - -func (c *Config) SetStage() { - c.Stage = GetEnv("STAGE", "development") - return -} - -func GetEnv(key, fallback string) string { - if value, ok := os.LookupEnv(key); ok { - return value - } - - return fallback + Enable bool `yaml:"enable"` } diff --git a/configuration/defaults/logs.yaml b/configuration/defaults/logs.yaml index c065688..f4cbbb5 100644 --- a/configuration/defaults/logs.yaml +++ b/configuration/defaults/logs.yaml @@ -1,7 +1,9 @@ defaults: logs: level: debug - debug: true + format: text + no_color: false + show_caller: true sentry: - enable: true - dsn: "" + enable: false + dsn: "" \ No newline at end of file diff --git a/fasthttp.go b/fasthttp.go new file mode 100644 index 0000000..8028cd0 --- /dev/null +++ b/fasthttp.go @@ -0,0 +1,7 @@ +package log + +type FHLogger struct{} + +func (fhl FHLogger) Printf(format string, v ...interface{}) { + logger.Printf(format, v...) +} diff --git a/go.mod b/go.mod index 8f891d6..aed5783 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,13 @@ -module github.com/spacetab-io/logs-go +module github.com/spacetab-io/logs-go/v2 -go 1.13 +go 1.14 require ( - github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 // indirect - github.com/evalphobia/logrus_sentry v0.8.2 - github.com/getsentry/raven-go v0.2.0 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/sirupsen/logrus v1.5.0 - github.com/stretchr/testify v1.5.1 + github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054 // indirect + github.com/getsentry/raven-go v0.2.0 + github.com/google/uuid v1.1.5 + github.com/json-iterator/go v1.1.10 + github.com/pkg/errors v0.9.1 + github.com/rs/zerolog v1.20.0 + github.com/stretchr/testify v1.3.0 ) diff --git a/go.sum b/go.sum index 8d5bb03..37e6fca 100644 --- a/go.sum +++ b/go.sum @@ -1,28 +1,35 @@ -github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 h1:JLaf/iINcLyjwbtTsCJjc6rtlASgHeIJPrB6QmwURnA= -github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= +github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054 h1:uH66TXeswKn5PW5zdZ39xEwfS9an067BirqA+P4QaLI= +github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/evalphobia/logrus_sentry v0.8.2 h1:dotxHq+YLZsT1Bb45bB5UQbfCh3gM/nFFetyN46VoDQ= -github.com/evalphobia/logrus_sentry v0.8.2/go.mod h1:pKcp+vriitUqu9KiWj/VRFbRfFNUwz95/UkgG8a6MNc= github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs= github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.1.5 h1:kxhtnfFVi+rYdOALN0B3k9UT86zVJKfBimRaciULW4I= +github.com/google/uuid v1.1.5/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sirupsen/logrus v1.5.0 h1:1N5EYkVAPEywqZRJd7cwnRtCb6xJx7NH3T3WUTF980Q= -github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.20.0 h1:38k9hgtUBdxFwE34yS8rTHmHBa4eN16E4DJlv177LNs= +github.com/rs/zerolog v1.20.0/go.mod h1:IzD0RJ65iWH0w97OQQebJEvTZYvsCUm9WVLWBQrJRjo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/logger.go b/logger.go index 908d3c4..4c36e61 100644 --- a/logger.go +++ b/logger.go @@ -1,75 +1,274 @@ -package logs +// Package log provides a global logger for zerolog. +package log import ( + "context" + "fmt" + "io" + stdlog "log" "os" "time" - "github.com/evalphobia/logrus_sentry" - "github.com/sirupsen/logrus" + "github.com/getsentry/raven-go" + "github.com/google/uuid" + "github.com/pkg/errors" + "github.com/rs/zerolog" ) -type Logger struct { - *logrus.Logger -} +const ( + defaultLevel = "debug" + ctxRequestIDKey = "request_id" +) + +// logger is the global logger. +var logger, _ = newZerolog(Config{Level: "debug", Format: FormatText}, os.Stdout) -//NewLogger is logrus instantiating wrapper. Returns configured logrus instance -func NewLogger(config *Config) (log *Logger, err error) { - log = &Logger{Logger: logrus.New()} - log.Formatter = &logrus.TextFormatter{ - TimestampFormat: time.RFC3339, - FullTimestamp: true, - DisableLevelTruncation: true, - QuoteEmptyFields: true, +// set global Zerolog logger +func Init(stage string, cfg Config, serviceAlias string, serviceVersion string, w io.Writer) (err error) { + if w == nil { + w = os.Stdout } - // Output to stdout instead of the default stderr - // Can be any io.Writer, see below for File example - log.Out = os.Stdout + if cfg.Format == "" { + cfg.Format = FormatText + } - // Flag for whether to log caller info (off by default) - log.ReportCaller = true + if cfg.Sentry == nil || !cfg.Sentry.Enable || cfg.Sentry.DSN == "" { + logger, err = newZerolog(cfg, w) + return err + } - if log.Level, err = logrus.ParseLevel(config.LogLevel); err != nil { - return nil, err + client, err := raven.New(cfg.Sentry.DSN) + if err != nil { + return err } - if config.Sentry != nil && !config.Sentry.Enable { - return log, nil + pr, pw := io.Pipe() + + go sentryPush(stage, serviceAlias, serviceVersion, client, pr) + + cfg.Format = FormatJSON + logger, err = newZerolog(cfg, io.MultiWriter(w, pw)) + + return err +} + +func newZerolog(cfg Config, w io.Writer) (logger zerolog.Logger, err error) { + // setup a global function that transforms any error passed to + // zerolog to an error with stack strace. + zerolog.ErrorMarshalFunc = func(err error) interface{} { + if cfg.Sentry == nil { + return err + } + + es := errWithStackTrace{ + Err: err.Error(), + } + + if _, ok := err.(stackTracer); !ok { + err = errors.WithStack(err) + } + + if cfg.Sentry != nil && cfg.Sentry.Enable { + es.Stacktrace = stackTraceToSentry(err.(stackTracer).StackTrace()) + } + + return &es } - if config.Stage == "" { - config.SetStage() + // UNIX Time is faster and smaller than most timestamps + // If you set zerolog.TimeFieldFormat to an empty string, + // logs will write with UNIX time + zerolog.TimeFieldFormat = time.RFC3339Nano + + // CallerSkipFrameCount is the number of stack frames to skip to find the caller. + zerolog.CallerSkipFrameCount = 2 + + output := w + + if cfg.Format == "text" { + // pretty print during development + out := zerolog.ConsoleWriter{Out: w, TimeFormat: zerolog.TimeFieldFormat, NoColor: cfg.NoColor} + + out.PartsOrder = []string{ + zerolog.TimestampFieldName, + zerolog.LevelFieldName, + zerolog.MessageFieldName, + zerolog.CallerFieldName, + } + + out.FormatMessage = func(i interface{}) string { + if i == nil { + return "" + } + + return fmt.Sprintf("|> %s <|", i) + } + + output = out } - if err := log.addSentryHook(config.Stage, config.Sentry); err != nil { - return nil, err + level, err := getLevel(cfg.Level) + if err != nil { + return logger, err } - return log, nil + logger = zerolog.New(output).With().Timestamp().Caller().Logger().Level(level) + + stdlog.SetFlags(0) + stdlog.SetOutput(logger) + + return logger, nil } -func (l *Logger) addSentryHook(stage string, cfg *SentryConfig) error { - sentryLevels := []logrus.Level{ - logrus.WarnLevel, - logrus.ErrorLevel, - logrus.FatalLevel, - logrus.PanicLevel, +func getLevel(lvl string) (zerolog.Level, error) { + if lvl == "" { + lvl = defaultLevel } - hook, err := logrus_sentry.NewAsyncSentryHook(cfg.DSN, sentryLevels) + level, err := zerolog.ParseLevel(lvl) if err != nil { - return err + return zerolog.DebugLevel, err } - hook.SetEnvironment(stage) - hook.StacktraceConfiguration.Enable = true - hook.StacktraceConfiguration.Level = logrus.WarnLevel - hook.StacktraceConfiguration.Skip = 6 - hook.StacktraceConfiguration.Context = 10 - hook.StacktraceConfiguration.IncludeErrorBreadcrumb = true - hook.StacktraceConfiguration.SendExceptionType = true + return level, nil +} + +func Logger() zerolog.Logger { + return logger +} + +// Output duplicates the global logger and sets w as its output. +func Output(w io.Writer) zerolog.Logger { + return logger.Output(w) +} + +// With creates a child logger with the field added to its context. +func With() zerolog.Context { + return logger.With() +} + +// Level creates a child logger with the minimum accepted level set to level. +func Level(level zerolog.Level) zerolog.Logger { + return logger.Level(level) +} + +// Sample returns a logger with the s sampler. +func Sample(s zerolog.Sampler) zerolog.Logger { + return logger.Sample(s) +} + +// Hook returns a logger with the h Hook. +func Hook(h zerolog.Hook) zerolog.Logger { + return logger.Hook(h) +} + +// Err starts a new message with error level with err as a field if not nil or +// with info level if err is nil. +// +// You must call Msg on the returned event in order to send the event. +func Err(err error) *zerolog.Event { + return logger.Err(err) +} + +// Trace starts a new message with trace level. +// +// You must call Msg on the returned event in order to send the event. +func Trace() *zerolog.Event { + return logger.Trace() +} + +// Debug starts a new message with debug level. +// +// You must call Msg on the returned event in order to send the event. +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() +} + +// WithLevel starts a new message with level. +// +// You must call Msg on the returned event in order to send the event. +func WithLevel(level zerolog.Level) *zerolog.Event { + return logger.WithLevel(level) +} + +// Log starts a new message with no level. Setting zerolog.GlobalLevel to +// zerolog.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() +} + +// Print sends a log event using debug level and no extra field. +// Arguments are handled in the manner of fmt.Print. +func Print(v ...interface{}) { + logger.Print(v...) +} + +// Printf sends a log event using debug level and no extra field. +// Arguments are handled in the manner of fmt.Printf. +func Printf(format string, v ...interface{}) { + logger.Printf(format, v...) +} + +// Ctx returns the logger associated with the ctx. If no logger +// is associated, a disabled logger is returned. +func Ctx(ctx context.Context) *zerolog.Logger { + return zerolog.Ctx(ctx) +} + +func contextFields(ctx context.Context) (fields map[string]interface{}) { + fields = make(map[string]interface{}) + if requestID, ok := ctx.Value(ctxRequestIDKey).(uuid.UUID); ok && requestID != uuid.Nil { + fields[ctxRequestIDKey] = requestID + } + + return fields +} - l.Hooks.Add(hook) +// With creates a child logger with the field added to its context. +func WithCtx(ctx context.Context) *zerolog.Logger { + l := With() + fields := contextFields(ctx) + l2 := l.Fields(fields).Logger() - return nil + return &l2 } diff --git a/logger_test.go b/logger_test.go index bcbf295..431c287 100644 --- a/logger_test.go +++ b/logger_test.go @@ -1,157 +1,92 @@ -package logs +package log import ( - "os" + "bytes" + "errors" + "io" "testing" - "time" - "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" ) -var ( - testFormatter = &logrus.TextFormatter{ - TimestampFormat: time.RFC3339, - FullTimestamp: true, - DisableLevelTruncation: true, - QuoteEmptyFields: true, +func TestTrace(t *testing.T) { + out := &bytes.Buffer{} + err := initLog(out) + if !assert.NoError(t, err, "logger init") { + t.FailNow() } -) -// I just want to test it before pushing. Don't know how to test it in right way, sorry ) -func TestNewLogger(t *testing.T) { - type testCase struct { - name string - config *Config - expFormatte *logrus.TextFormatter - hasErr bool - } - type testCases []testCase + Trace().Msg("trace") + exp := "TRC |> trace <|" + assert.Contains(t, out.String(), exp) +} - cfg := &Config{ - Stage: "test", - LogLevel: logrus.InfoLevel.String(), - Sentry: &SentryConfig{ - Enable: false, - }, - } - cfg2 := &Config{ - Stage: "test", - LogLevel: logrus.InfoLevel.String(), - Sentry: &SentryConfig{ - Enable: true, - DSN: "https://xxx@sentry.io/yyy", - }, +func TestDebug(t *testing.T) { + out := &bytes.Buffer{} + err := initLog(out) + if !assert.NoError(t, err, "logger init") { + t.FailNow() } - cfg3 := &Config{ - Stage: "", - LogLevel: logrus.InfoLevel.String(), - Sentry: &SentryConfig{ - Enable: true, - DSN: "go away", - }, - } - cfg4 := &Config{ - Stage: "test", - LogLevel: logrus.InfoLevel.String(), - Sentry: &SentryConfig{ - Enable: true, - DSN: "go away", - }, - } - cfg5 := &Config{ - Stage: "test", - LogLevel: "paranoya", - Sentry: &SentryConfig{ - Enable: true, - DSN: "go away", - }, - } + Debug().Msg("debug") + exp := "DBG |> debug <|" + assert.Contains(t, out.String(), exp) +} - tcs := testCases{ - {name: "new logger", config: cfg, expFormatte: testFormatter}, - {name: "good sentry", config: cfg2, expFormatte: testFormatter}, - {name: "no stage", config: cfg3, hasErr: true}, - {name: "bad sentry dsn", config: cfg4, hasErr: true}, - {name: "bad sentry log lvl", config: cfg5, hasErr: true}, +func TestInfo(t *testing.T) { + out := &bytes.Buffer{} + err := initLog(out) + if !assert.NoError(t, err, "logger init") { + t.FailNow() } - for _, tc := range tcs { - t.Run(tc.name, func(t *testing.T) { - l, err := NewLogger(tc.config) - if tc.hasErr { - if !assert.Error(t, err, "has error fail") { - t.FailNow() - } - return - } else { - if !assert.NoError(t, err, "no error fail") { - t.FailNow() - } - } + Info().Msg("info") + exp := "INF |> info <|" + assert.Contains(t, out.String(), exp) +} - if !assert.Equal(t, tc.config.LogLevel, l.GetLevel().String(), "wrong log level") { - t.FailNow() - } +func TestWarn(t *testing.T) { + out := &bytes.Buffer{} + err := initLog(out) + if !assert.NoError(t, err, "logger init") { + t.FailNow() + } - if !assert.Equal(t, tc.expFormatte, l.Formatter, "differ formatter") { - t.FailNow() - } + Warn().Msg("warn") + exp := "WRN |> warn <|" + assert.Contains(t, out.String(), exp) +} - if tc.config.Sentry.Enable { - if !assert.Equal(t, 4, len(l.Hooks), "wrong number of hooks") { - t.FailNow() - } - } - }) +func TestError(t *testing.T) { + out := &bytes.Buffer{} + err := initLog(out) + if !assert.NoError(t, err, "logger init") { + t.FailNow() } -} -func TestGetEnv(t *testing.T) { - t.Run("get ent key value", func(t *testing.T) { - c := &Config{} - key := "STAGE" - val := "testing" - err := os.Setenv(key, val) - assert.NoError(t, err) - c.SetStage() - assert.Equal(t, val, c.Stage) - _ = os.Unsetenv(key) - }) - t.Run("get fallback stage", func(t *testing.T) { - c := &Config{} - key := "notSTAGE" - val := "testing" - err := os.Setenv(key, val) - assert.NoError(t, err) - c.SetStage() - assert.Equal(t, "development", c.Stage) - }) + Error().Msg("error") + exp := "ERR |> error <|" + assert.Contains(t, out.String(), exp) } -func TestConfig_SetStage(t *testing.T) { - type testCase struct { - name string - key string - flb string - val string - hasErr bool +func TestErr(t *testing.T) { + out := &bytes.Buffer{} + err := initLog(out) + if !assert.NoError(t, err, "logger init") { + t.FailNow() } - type testCases []testCase - tcs := testCases{ - {name: "get env value", key: "key", flb: "", val: "value", hasErr: false}, - {name: "get fallback value", key: "", flb: "fallback", val: "fallback", hasErr: false}, - } + Error().Err(errors.New("some err")).Msg("error") + exp := "ERR |> error <| logger_test.go:79 > error=\"some err\"" + assert.Contains(t, out.String(), exp) +} - for _, tc := range tcs { - t.Run(tc.name, func(t *testing.T) { - os.Setenv(tc.key, tc.val) - val := GetEnv(tc.key, tc.flb) - if !assert.Equal(t, tc.val, val) { - t.FailNow() - } - }) - } +func initLog(w io.Writer) error { + return Init("test", Config{ + Level: "trace", + Format: "text", + NoColor: true, + ShowCaller: true, + Sentry: nil, + }, "log", "v2.*.*", w) } diff --git a/sentry_hook.go b/sentry_hook.go new file mode 100644 index 0000000..b10c710 --- /dev/null +++ b/sentry_hook.go @@ -0,0 +1,146 @@ +package log + +import ( + "fmt" + "io" + "os" + "runtime" + "time" + + "github.com/getsentry/raven-go" + jsoniter "github.com/json-iterator/go" + "github.com/pkg/errors" +) + +type errWithStackTrace struct { + Err string `json:"error"` + Stacktrace *raven.Stacktrace `json:"stacktrace"` +} + +type sentryEvent struct { + Level string `json:"level"` + Msg string `json:"message"` + Err errWithStackTrace `json:"error"` + Time time.Time `json:"time"` + Status int `json:"status,omitempty"` + UserAgent string `json:"user_agent,omitempty"` + Method string `json:"method,omitempty"` + URL string `json:"url,omitempty"` + IP string `json:"ip,omitempty"` + RequestID string `json:"request_id,omitempty"` + Action string `json:"action,omitempty"` +} + +var errSkipEvent = errors.New("skip") + +func sentryPush(stage string, serviceAlias string, serviceVersion string, client *raven.Client, pr io.Reader) { + defer client.Close() + + var json = jsoniter.ConfigCompatibleWithStandardLibrary + dec := json.NewDecoder(pr) + + for { + var e sentryEvent + + err := dec.Decode(&e) + + switch err { + case nil: + break + case io.EOF: + return + case errSkipEvent: + continue + default: + _, _ = fmt.Fprintf(os.Stderr, "unmarshaling logger failed with error %v\n", err) + continue + } + + var level raven.Severity + + switch e.Level { + case "debug": + level = raven.DEBUG + case "info": + level = raven.INFO + case "warn": + level = raven.WARNING + case "error": + level = raven.ERROR + case "fatal", "panic": + level = raven.FATAL + default: + continue + } + + packet := raven.Packet{ + Message: e.Msg, + Timestamp: raven.Timestamp(e.Time), + Level: level, + Platform: "go", + Project: serviceAlias, + Logger: "zerolog", + Release: serviceVersion, + Culprit: e.Err.Err, + Environment: stage, + } + + if e.Err.Stacktrace != nil { + packet.Interfaces = append(packet.Interfaces, e.Err.Stacktrace) + } + + if e.IP != "" { + packet.Interfaces = append(packet.Interfaces, &raven.User{IP: e.IP}) + } + + if e.URL != "" { + h := raven.Http{ + URL: e.URL, + Method: e.Method, + Headers: make(map[string]string), + } + if e.UserAgent != "" { + h.Headers["User-Agent"] = e.UserAgent + } + + packet.Interfaces = append(packet.Interfaces, &h) + } + + _, _ = client.Capture(&packet, nil) + } +} + +type stackTracer interface { + StackTrace() errors.StackTrace +} + +func stackTraceToSentry(st errors.StackTrace) *raven.Stacktrace { + var frames []*raven.StacktraceFrame + + for _, f := range st { + pc := uintptr(f) - 1 + fn := runtime.FuncForPC(pc) + + var ( + funcName, file string + line int + ) + + unk := "unknown" + + if fn != nil { + file, line = fn.FileLine(pc) + funcName = fn.Name() + } else { + file = unk + funcName = unk + } + + frame := raven.NewStacktraceFrame(pc, funcName, file, line, 3, nil) + if frame != nil { + frames = append([]*raven.StacktraceFrame{frame}, frames...) + } + } + + return &raven.Stacktrace{Frames: frames} +}