Skip to content

Commit

Permalink
feat(pkg/logging): add logging package (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
aqyuki authored Dec 23, 2024
1 parent da9d75b commit df32bc4
Show file tree
Hide file tree
Showing 4 changed files with 378 additions and 0 deletions.
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
module github.com/aqyuki/felm

go 1.23.4

require go.uber.org/zap v1.27.0

require go.uber.org/multierr v1.10.0 // indirect
14 changes: 14 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
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/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/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
171 changes: 171 additions & 0 deletions pkg/logging/logger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package logging

import (
"context"
"os"
"strings"
"sync"
"time"

"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)

type contextKey string

const loggerKey = contextKey("logger")

var (
defaultLogger *zap.Logger
defaultLoggerOnce sync.Once
)

func NewLoggerFromEnv() *zap.Logger {
develop := strings.ToLower(strings.TrimSpace(os.Getenv("LOG_MODE"))) == "develop"
level := strings.TrimSpace(os.Getenv("LOG_LEVEL"))
return NewLogger(develop, level)
}

func NewLogger(develop bool, level string) *zap.Logger {
var cfg *zap.Config
if develop {
cfg = &zap.Config{
Level: zap.NewAtomicLevelAt(levelToZapLevel(level)),
Development: true,
Encoding: encodingConsole,
OutputPaths: outputStderr,
ErrorOutputPaths: outputStderr,
EncoderConfig: developmentEncoderConfig,
}
} else {
cfg = &zap.Config{
Level: zap.NewAtomicLevelAt(levelToZapLevel(level)),
Encoding: encodingJSON,
EncoderConfig: productionEncoderConfig,
OutputPaths: outputStderr,
ErrorOutputPaths: outputStderr,
}
}
logger, err := cfg.Build()
if err != nil {
logger = zap.NewNop()
}
return logger
}

func DefaultLogger() *zap.Logger {
defaultLoggerOnce.Do(func() {
defaultLogger = NewLoggerFromEnv()
})
return defaultLogger
}

func WithLogger(ctx context.Context, logger *zap.Logger) context.Context {
return context.WithValue(ctx, loggerKey, logger)
}

func FromContext(ctx context.Context) *zap.Logger {
if logger, ok := ctx.Value(loggerKey).(*zap.Logger); ok {
return logger
}
return DefaultLogger()
}

const (
timestamp = "timestamp"
severity = "severity"
logger = "logger"
caller = "caller"
message = "message"
stacktrace = "stacktrace"

levelDebug = "DEBUG"
levelInfo = "INFO"
levelWarning = "WARNING"
levelError = "ERROR"
levelCritical = "CRITICAL"
levelAlert = "ALERT"
levelEmergency = "EMERGENCY"

encodingConsole = "console"
encodingJSON = "json"
)

var outputStderr = []string{"stderr"}

var productionEncoderConfig = zapcore.EncoderConfig{
TimeKey: timestamp,
LevelKey: severity,
NameKey: logger,
CallerKey: caller,
MessageKey: message,
StacktraceKey: stacktrace,
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: levelEncoder(),
EncodeTime: timeEncoder(),
EncodeDuration: zapcore.SecondsDurationEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
}

var developmentEncoderConfig = zapcore.EncoderConfig{
TimeKey: "",
LevelKey: "L",
NameKey: "N",
CallerKey: "C",
FunctionKey: zapcore.OmitKey,
MessageKey: "M",
StacktraceKey: "S",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.CapitalLevelEncoder,
EncodeTime: zapcore.ISO8601TimeEncoder,
EncodeDuration: zapcore.StringDurationEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
}

func levelToZapLevel(level string) zapcore.Level {
switch strings.ToUpper(level) {
case levelDebug:
return zapcore.DebugLevel
case levelInfo:
return zapcore.InfoLevel
case levelWarning:
return zapcore.WarnLevel
case levelError:
return zapcore.ErrorLevel
case levelCritical:
return zapcore.DPanicLevel
case levelAlert:
return zapcore.PanicLevel
case levelEmergency:
return zapcore.FatalLevel
default:
return zapcore.InfoLevel
}
}

func levelEncoder() zapcore.LevelEncoder {
return func(l zapcore.Level, enc zapcore.PrimitiveArrayEncoder) {
switch l {
case zapcore.DebugLevel:
enc.AppendString(levelDebug)
case zapcore.InfoLevel:
enc.AppendString(levelInfo)
case zapcore.WarnLevel:
enc.AppendString(levelWarning)
case zapcore.ErrorLevel:
enc.AppendString(levelError)
case zapcore.DPanicLevel:
enc.AppendString(levelCritical)
case zapcore.PanicLevel:
enc.AppendString(levelAlert)
case zapcore.FatalLevel:
enc.AppendString(levelEmergency)
}
}
}

func timeEncoder() zapcore.TimeEncoder {
return func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendString(t.Format(time.RFC3339Nano))
}
}
189 changes: 189 additions & 0 deletions pkg/logging/logger_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package logging

import (
"context"
"testing"
"time"

"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)

func TestNewLoggerFromEnv(t *testing.T) {
t.Run("When LOG_MODE is develop", func(t *testing.T) {
t.Setenv("LOG_MODE", "develop")
actual := NewLoggerFromEnv()

if actual == nil {
t.Errorf("Expected logger to be not nil, but got nil")
}
})

t.Run("When LOG_MODE is not develop", func(t *testing.T) {
t.Setenv("LOG_MODE", "production")
actual := NewLoggerFromEnv()

if actual == nil {
t.Errorf("Expected logger to be not nil, but got nil")
}
})
}

func TestNewLogger(t *testing.T) {
t.Parallel()
t.Run("When develop is true", func(t *testing.T) {
t.Parallel()
actual := NewLogger(true, "debug")

if actual == nil {
t.Errorf("Expected logger to be not nil, but got nil")
}
})

t.Run("When develop is false", func(t *testing.T) {
t.Parallel()
actual := NewLogger(false, "info")

if actual == nil {
t.Errorf("Expected logger to be not nil, but got nil")
}
})
}

func TestDefaultLogger(t *testing.T) {
t.Parallel()

actual1 := DefaultLogger()
actual2 := DefaultLogger()

if actual1 == nil || actual2 == nil {
t.Errorf("Expected both loggers to be not nil")
}
if actual1 != actual2 {
t.Errorf("Expected both loggers to be the same, but got different instances")
}
}

func TestWithLogger(t *testing.T) {
t.Parallel()
logger := zap.NewNop()
ctx := WithLogger(context.Background(), logger)

if ctx == nil {
t.Errorf("Expected context with logger to be not nil, but got nil")
}
}

func TestFromContext(t *testing.T) {
t.Parallel()

t.Run("When logger is set in context", func(t *testing.T) {
t.Parallel()
logger := zap.NewNop()
ctx := WithLogger(context.Background(), logger)
actual := FromContext(ctx)

if actual == nil {
t.Errorf("Expected logger from context to be not nil, but got nil")
}
})

t.Run("When logger is not set in context", func(t *testing.T) {
t.Parallel()
ctx := context.Background()
actual := FromContext(ctx)

if actual == nil {
t.Errorf("Expected default logger to be returned, but got nil")
}
})
}

func Test_levelToZapLevel(t *testing.T) {
t.Parallel()

tests := []struct {
name string
level string
want zapcore.Level
}{
{"debug", "debug", zapcore.DebugLevel},
{"info", "info", zapcore.InfoLevel},
{"warning", "warning", zapcore.WarnLevel},
{"error", "error", zapcore.ErrorLevel},
{"critical", "critical", zapcore.DPanicLevel},
{"alert", "alert", zapcore.PanicLevel},
{"emergency", "emergency", zapcore.FatalLevel},
{"unknown", "unknown", zapcore.InfoLevel},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
actual := levelToZapLevel(tt.level)
if actual != tt.want {
t.Errorf("Expected level %v, but got %v", tt.want, actual)
}
})
}
}

func Test_levelEncoder(t *testing.T) {
t.Parallel()

tests := []struct {
name string
level zapcore.Level
want string
}{
{"debug", zapcore.DebugLevel, "DEBUG"},
{"info", zapcore.InfoLevel, "INFO"},
{"warning", zapcore.WarnLevel, "WARNING"},
{"error", zapcore.ErrorLevel, "ERROR"},
{"critical", zapcore.DPanicLevel, "CRITICAL"},
{"alert", zapcore.PanicLevel, "ALERT"},
{"emergency", zapcore.FatalLevel, "EMERGENCY"},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
mem := zapcore.NewMapObjectEncoder()
err := mem.AddArray("k", zapcore.ArrayMarshalerFunc(func(arr zapcore.ArrayEncoder) error {
levelEncoder()(tt.level, arr)
return nil
}))
if err != nil {
t.Errorf("Expected no error, but got %v", err)
}
arr := mem.Fields["k"].([]any)
if len(arr) != 1 {
t.Errorf("Expected array length 1, but got %d", len(arr))
}
if arr[0] != tt.want {
t.Errorf("Expected %v, but got %v", tt.want, arr[0])
}
})
}
}

func Test_timeEncoder(t *testing.T) {
t.Parallel()
moment := time.Unix(100, 50005000).UTC()
mem := zapcore.NewMapObjectEncoder()
err := mem.AddArray("k", zapcore.ArrayMarshalerFunc(func(arr zapcore.ArrayEncoder) error {
timeEncoder()(moment, arr)
return nil
}))
if err != nil {
t.Errorf("Expected no error, but got %v", err)
}
arr := mem.Fields["k"].([]any)
if len(arr) != 1 {
t.Errorf("Expected array length 1, but got %d", len(arr))
}
expected := "1970-01-01T00:01:40.050005Z"
if arr[0] != expected {
t.Errorf("Expected %v, but got %v", expected, arr[0])
}
}

0 comments on commit df32bc4

Please sign in to comment.