-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
231 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 |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package log_test | ||
|
||
import ( | ||
"log/slog" | ||
|
||
"github.com/elisasre/go-common/log" | ||
) | ||
|
||
func ExampleNewDefaultLogger() { | ||
log.NewDefaultLogger() | ||
slog.Info("Hello world") | ||
slog.Error("Some error") | ||
} |
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,97 @@ | ||
// Package log provides sane default loggers using slog. | ||
package log | ||
|
||
import ( | ||
"io" | ||
"log/slog" | ||
"os" | ||
"strings" | ||
"time" | ||
) | ||
|
||
const DefaultRefreshInterval = time.Second * 5 | ||
|
||
// NewDefaultEnvLogger creates new slog.Logger using sane default configuration and sets it as a default logger. | ||
// Environment variables can be used to configure loggers format and level. Changing log level at runtime is also supported. | ||
// | ||
// Name: Value: | ||
// LOG_LEVEL DEBUG|INFO|WARN|ERROR | ||
// LOG_FORMAT JSON|TEXT | ||
// | ||
// Note: LOG_FORMAT can't be changed at runtime. | ||
func NewDefaultLogger() *slog.Logger { | ||
lvl := &slog.LevelVar{} | ||
lvl.Set(ParseLogLevelFromEnv()) | ||
go RefreshLogLevel(lvl, time.NewTicker(DefaultRefreshInterval)) | ||
|
||
handlerFn := ParseFormatEnv() | ||
opts := &slog.HandlerOptions{ | ||
AddSource: true, | ||
Level: lvl, | ||
} | ||
|
||
logger := slog.New(handlerFn(os.Stdout, opts)) | ||
slog.SetDefault(logger) | ||
|
||
return logger | ||
} | ||
|
||
// HandlerFn is a shim type for slog's NewHandler functions. | ||
type HandlerFn func(w io.Writer, opts *slog.HandlerOptions) slog.Handler | ||
|
||
// JSONHandler is a LogHandlerFn shim for slog.NewJSONHandler. | ||
func JSONHandler(w io.Writer, opts *slog.HandlerOptions) slog.Handler { | ||
return slog.NewJSONHandler(w, opts) | ||
} | ||
|
||
// TextHandler is a LogHandlerFn shim for slog.NewTextHandler. | ||
func TextHandler(w io.Writer, opts *slog.HandlerOptions) slog.Handler { | ||
return slog.NewTextHandler(w, opts) | ||
} | ||
|
||
// ParseFormat parses string into supported log handler function. | ||
// If the input doesn't match to any supported format then JSON is used. | ||
func ParseFormat(format string) HandlerFn { | ||
switch strings.ToUpper(format) { | ||
case "JSON": | ||
return JSONHandler | ||
case "TEXT": | ||
return TextHandler | ||
default: | ||
return JSONHandler | ||
} | ||
} | ||
|
||
// ParseFormatEnv turns LOG_FORMAT env variable into slog.Handler function using ParseLogFormat. | ||
func ParseFormatEnv() HandlerFn { | ||
return ParseFormat(os.Getenv("LOG_FORMAT")) | ||
} | ||
|
||
// ParseFormat turns string into slog.Level using case-insensitive parser. | ||
// If the input doesn't match to any slog.Level then slog.LevelInfo is used. | ||
func ParseLogLevel(level string) slog.Level { | ||
switch strings.ToUpper(level) { | ||
case "DEBUG": | ||
return slog.LevelDebug | ||
case "INFO": | ||
return slog.LevelInfo | ||
case "WARN": | ||
return slog.LevelWarn | ||
case "ERROR": | ||
return slog.LevelError | ||
default: | ||
return slog.LevelInfo | ||
} | ||
} | ||
|
||
// ParseLogLevelFromEnv turns LOG_LEVEL env variable into slog.Level using logic from ParseLogLevel. | ||
func ParseLogLevelFromEnv() slog.Level { | ||
return ParseLogLevel(os.Getenv("LOG_LEVEL")) | ||
} | ||
|
||
// RefreshLogLevel updates l's value from env with given interval until ticker is stopped. | ||
func RefreshLogLevel(l *slog.LevelVar, t *time.Ticker) { | ||
for range t.C { | ||
l.Set(ParseLogLevelFromEnv()) | ||
} | ||
} |
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,121 @@ | ||
// Package log provides sane default loggers using slog. | ||
package log_test | ||
|
||
import ( | ||
"bufio" | ||
"context" | ||
"fmt" | ||
"log/slog" | ||
"testing" | ||
"time" | ||
|
||
"github.com/elisasre/go-common/log" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestParseLogLevel(t *testing.T) { | ||
tests := []struct { | ||
input string | ||
expected slog.Level | ||
}{ | ||
{ | ||
input: "", | ||
expected: slog.LevelInfo, | ||
}, | ||
{ | ||
input: "info", | ||
expected: slog.LevelInfo, | ||
}, | ||
{ | ||
input: "INFO", | ||
expected: slog.LevelInfo, | ||
}, | ||
{ | ||
input: "DEBUG", | ||
expected: slog.LevelDebug, | ||
}, | ||
{ | ||
input: "WARN", | ||
expected: slog.LevelWarn, | ||
}, | ||
{ | ||
input: "ERROR", | ||
expected: slog.LevelError, | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
gotLevel := log.ParseLogLevel(tt.input) | ||
assert.Equal(t, tt.expected, gotLevel) | ||
} | ||
} | ||
|
||
func TestParseFormat(t *testing.T) { | ||
tests := []struct { | ||
input string | ||
expected log.HandlerFn | ||
}{ | ||
{ | ||
input: "", | ||
expected: log.JSONHandler, | ||
}, | ||
{ | ||
input: "json", | ||
expected: log.JSONHandler, | ||
}, | ||
{ | ||
input: "JSON", | ||
expected: log.JSONHandler, | ||
}, | ||
{ | ||
input: "TEXT", | ||
expected: log.TextHandler, | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
handlerFn := log.ParseFormat(tt.input) | ||
assert.Equal(t, fmt.Sprint(tt.expected), fmt.Sprint(handlerFn)) | ||
handlerFn(bufio.NewWriter(nil), nil) | ||
} | ||
} | ||
|
||
func TestRefreshLogLevel(t *testing.T) { | ||
l := &slog.LevelVar{} | ||
tick := time.NewTicker(time.Millisecond) | ||
done := make(chan struct{}) | ||
go func() { | ||
defer close(done) | ||
log.RefreshLogLevel(l, tick) | ||
}() | ||
|
||
t.Setenv("LOG_LEVEL", "INFO") | ||
time.Sleep(time.Millisecond * 10) | ||
require.Equal(t, "INFO", l.Level().String()) | ||
|
||
t.Setenv("LOG_LEVEL", "DEBUG") | ||
time.Sleep(time.Millisecond * 10) | ||
require.Equal(t, "DEBUG", l.Level().String()) | ||
|
||
tick.Stop() | ||
time.Sleep(time.Millisecond * 10) | ||
|
||
t.Setenv("LOG_LEVEL", "INFO") | ||
time.Sleep(time.Millisecond * 10) | ||
require.Equal(t, "DEBUG", l.Level().String()) | ||
} | ||
|
||
func TestNewDefaultLogger(t *testing.T) { | ||
logger := log.NewDefaultLogger() | ||
require.Equal(t, logger, slog.Default()) | ||
|
||
debugEnabled := logger.Handler().Enabled(context.Background(), slog.LevelDebug) | ||
require.False(t, debugEnabled) | ||
|
||
t.Setenv("LOG_LEVEL", "debug") | ||
time.Sleep(time.Second * 6) | ||
|
||
debugEnabled = logger.Handler().Enabled(context.Background(), slog.LevelDebug) | ||
require.True(t, debugEnabled) | ||
} |