Skip to content

Commit

Permalink
Add slog utils
Browse files Browse the repository at this point in the history
  • Loading branch information
heppu committed Oct 27, 2023
1 parent 2b4d699 commit d86f6b7
Show file tree
Hide file tree
Showing 3 changed files with 231 additions and 0 deletions.
13 changes: 13 additions & 0 deletions log/example_test.go
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")
}
97 changes: 97 additions & 0 deletions log/logger.go
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())
}
}
121 changes: 121 additions & 0 deletions log/logger_test.go
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)
}

0 comments on commit d86f6b7

Please sign in to comment.