-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(pkg/logging): add logging package (#3)
- Loading branch information
Showing
4 changed files
with
378 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]) | ||
} | ||
} |