diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..3012e3a --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @yuseferi diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b86faaa --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,38 @@ +# This workflow will build a golang project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go +name: Check & Build +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + qa: + name: Quality check + timeout-minutes: 10 + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Setup Go + uses: actions/setup-go@v4 + - name: Linter + uses: golangci/golangci-lint-action@v3 + with: + version: v1.53.2 + - name: Tests + run: go mod download; go test -race ./... + build: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v4 + - name: Build + run: go mod download; go build -v ./... diff --git a/.gitignore b/.gitignore index 3b735ec..f50e6e1 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,5 @@ # Go workspace file go.work +.vs/ +.idea/ diff --git a/README.md b/README.md index 27c0eba..abb315b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,61 @@ -# zax -Zap logger with context +# Zax (zap with context) + +Basically this adds context to [Zap Logger](https://github.com/uber-go/zap), and make it easier to for the Gophers to do not generate logger boiler plates. +Passing logger as a parameter to function increase parameters functionalities and worse than couple lots of methods with a explicit dependency. + + +### Installation + +```shell + go get -u github.com/yuseferi/zax +``` + +### Usage: +when you add something to context and would like to carry with context , you just need to add it to context with calling `zap.Set` + + ctx = zax.Set(ctx, logger, []zap.Field{zap.String("trace_id", "my-trace-id")}) + +and when you want to log context fields, just use + + zax.Get(ctx) + + + +##### example: +you want to generate a tracer on entry point of your system and want to keep it until the process finished. + +```Go +func main() { + logger, _ := zap.NewProduction() + ctx := context.Background() + s := NewServiceA(logger) + ctx = zax.Set(ctx, logger, []zap.Field{zap.String("trace_id", "my-trace-id")}) + s.funcA(ctx) +} + +type ServiceA struct { +logger *zap.Logger +} + +func NewServiceA(logger *zap.Logger) *ServiceA { + return &ServiceA{ + logger: logger, + } +} + +func (s *ServiceA) funcA(ctx context.Context) { + s.logger.Info("func A") // it does not contain trace_id, you need to add it manually + zax.Get(ctx).Info("func A") // it will logged with "trace_id" = "my-trace-id" +} + +``` + +### Contributing +I strongly believe in open-source :), feel free to make it better with raising issues and PRs. + + +Released under the [GNU GENERAL PUBLIC LICENSE](LICENSE). + + + + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..13fc53d --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module zax + +go 1.20 + +require ( + github.com/stretchr/testify v1.8.0 + go.uber.org/zap v1.24.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b6c51c0 --- /dev/null +++ b/go.sum @@ -0,0 +1,25 @@ +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +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/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +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.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/zax.go b/zax.go new file mode 100644 index 0000000..350a934 --- /dev/null +++ b/zax.go @@ -0,0 +1,29 @@ +// Package zax provides contextual field logging around the uber-zap logger. + +package zax + +import ( + "context" + "go.uber.org/zap" +) + +type Key string + +// Key name which used for save logger in context +const loggerKey = Key("zax") + +// Set Add passed fields to logger and store zap.Logger as variable in context +func Set(ctx context.Context, logger *zap.Logger, fields []zap.Field) context.Context { + if len(fields) > 0 { + logger = logger.With(fields...) + } + return context.WithValue(ctx, loggerKey, logger) +} + +// Get zap.Logger from context +func Get(ctx context.Context) *zap.Logger { + if logger, ok := ctx.Value(loggerKey).(*zap.Logger); ok { + return logger + } + return zap.L() +} diff --git a/zax_test.go b/zax_test.go new file mode 100644 index 0000000..69df6b3 --- /dev/null +++ b/zax_test.go @@ -0,0 +1,123 @@ +package zax + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "go.uber.org/zap/zaptest/observer" +) + +type Logger struct { + logger *zap.Logger + recorded *observer.ObservedLogs + t *testing.T +} + +func NewLogger(t *testing.T) *Logger { + core, recorded := observer.New(zapcore.DebugLevel) + logger := &Logger{ + logger: zap.New(core), + recorded: recorded, + t: t, + } + return logger +} + +func (l *Logger) GetZapLogger() *zap.Logger { + return l.logger +} + +func (l *Logger) GetRecordedLogs() []observer.LoggedEntry { + return l.recorded.All() +} + +func (l *Logger) AssertLogEntryExist(t assert.TestingT, key, value string) bool { + for _, log := range l.recorded.All() { + for _, r := range log.Context { + if r.Key == key && r.String == value { + return true + } + } + } + return assert.Fail(t, fmt.Sprintf("log entry does not exist with, %s = %s", key, value)) +} + +func (l *Logger) AssertLogEntryKeyExist(t assert.TestingT, key string) bool { + for _, log := range l.recorded.All() { + for _, r := range log.Context { + if r.Key == key { + return true + } + } + } + return assert.Fail(t, fmt.Sprintf("log entry does not exist with key = %s ", key)) +} + +const traceIDKey = "trace_id" + +func TestSet(t *testing.T) { + testLog := NewLogger(t) + testTraceID := "test-trace-id-3333" + testTraceID2 := "test-trace-id-new" + ctx := context.Background() + tests := map[string]struct { + context context.Context + expectedLoggerKey string + expectedLoggerValue string + }{ + "context with trace-id": { + context: Set(ctx, testLog.logger, []zap.Field{zap.String(traceIDKey, testTraceID)}), + expectedLoggerKey: traceIDKey, + expectedLoggerValue: testTraceID, + }, + "context with trace-id with new value(to check it will be updated)": { + context: Set(ctx, testLog.logger, []zap.Field{zap.String(traceIDKey, testTraceID2)}), + expectedLoggerKey: traceIDKey, + expectedLoggerValue: testTraceID2, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + ctx := tc.context + logger := ctx.Value(loggerKey).(*zap.Logger) + logger.Info("just a test record") + assert.NotNil(t, logger) + testLog.AssertLogEntryExist(t, tc.expectedLoggerKey, tc.expectedLoggerValue) + }) + } +} + +func TestGet(t *testing.T) { + testLog := NewLogger(t) + testTraceID := "test-trace-id-3333" + traceIDKey := traceIDKey + ctx := context.Background() + tests := map[string]struct { + context context.Context + expectedLoggerKey *string + }{ + "context with trace-id": { + context: context.TODO(), + expectedLoggerKey: nil, + }, + "context with trace-id with new value(to check it will be updated)": { + context: Set(ctx, testLog.logger, []zap.Field{zap.String(traceIDKey, testTraceID)}), + expectedLoggerKey: &traceIDKey, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + ctx := tc.context + Get(ctx).Info("just a test record") + if tc.expectedLoggerKey != nil { + testLog.AssertLogEntryKeyExist(t, *tc.expectedLoggerKey) + } + }) + } +}