Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add slog utils #107

Merged
merged 1 commit into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 ExampleNewDefaultEnvLogger() {
log.NewDefaultEnvLogger()
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.
kraashen marked this conversation as resolved.
Show resolved Hide resolved
// 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 NewDefaultEnvLogger() *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.NewDefaultEnvLogger()
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)
}