mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-05-09 13:58:11 +00:00
feat: add new logger
This commit is contained in:
@@ -0,0 +1,157 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Logger struct {
|
||||||
|
HTTP zerolog.Logger
|
||||||
|
App zerolog.Logger
|
||||||
|
config model.LogConfig
|
||||||
|
base zerolog.Logger
|
||||||
|
audit zerolog.Logger
|
||||||
|
writer io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLogger() *Logger {
|
||||||
|
return &Logger{
|
||||||
|
writer: os.Stderr,
|
||||||
|
config: model.LogConfig{
|
||||||
|
Level: "error",
|
||||||
|
Json: true,
|
||||||
|
Streams: model.LogStreams{
|
||||||
|
HTTP: model.LogStreamConfig{
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
App: model.LogStreamConfig{
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
// No reason to enabled audit by default since it will be surpressed by the log level
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) WithConfig(cfg model.LogConfig) *Logger {
|
||||||
|
l.config = cfg
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) WithSimpleConfig() *Logger {
|
||||||
|
l.config = model.LogConfig{
|
||||||
|
Level: "info",
|
||||||
|
Json: false,
|
||||||
|
Streams: model.LogStreams{
|
||||||
|
HTTP: model.LogStreamConfig{Enabled: true},
|
||||||
|
App: model.LogStreamConfig{Enabled: true},
|
||||||
|
Audit: model.LogStreamConfig{Enabled: false},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) WithTestConfig() *Logger {
|
||||||
|
l.config = model.LogConfig{
|
||||||
|
Level: "trace",
|
||||||
|
Streams: model.LogStreams{
|
||||||
|
HTTP: model.LogStreamConfig{Enabled: true},
|
||||||
|
App: model.LogStreamConfig{Enabled: true},
|
||||||
|
Audit: model.LogStreamConfig{Enabled: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) WithWriter(writer io.Writer) *Logger {
|
||||||
|
l.writer = writer
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Init() {
|
||||||
|
base := log.With().
|
||||||
|
Timestamp().
|
||||||
|
Caller().
|
||||||
|
Logger().
|
||||||
|
Level(l.parseLogLevel(l.config.Level)).Output(l.writer)
|
||||||
|
|
||||||
|
if !l.config.Json {
|
||||||
|
base = base.Output(zerolog.ConsoleWriter{
|
||||||
|
Out: l.writer,
|
||||||
|
TimeFormat: time.RFC3339,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
l.base = base
|
||||||
|
l.audit = l.createLogger("audit", l.config.Streams.Audit)
|
||||||
|
l.HTTP = l.createLogger("http", l.config.Streams.HTTP)
|
||||||
|
l.App = l.createLogger("app", l.config.Streams.App)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) parseLogLevel(level string) zerolog.Level {
|
||||||
|
if level == "" {
|
||||||
|
return zerolog.InfoLevel
|
||||||
|
}
|
||||||
|
parsed, err := zerolog.ParseLevel(strings.ToLower(level))
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Str("level", level).Msg("Invalid log level, defaulting to error")
|
||||||
|
parsed = zerolog.ErrorLevel
|
||||||
|
}
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) createLogger(component string, cfg model.LogStreamConfig) zerolog.Logger {
|
||||||
|
if !cfg.Enabled {
|
||||||
|
return zerolog.Nop()
|
||||||
|
}
|
||||||
|
sub := l.base.With().Str("stream", component).Logger()
|
||||||
|
if cfg.Level != "" {
|
||||||
|
sub = sub.Level(l.parseLogLevel(cfg.Level))
|
||||||
|
}
|
||||||
|
return sub
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) AuditLoginSuccess(username, provider, ip string) {
|
||||||
|
l.audit.Info().
|
||||||
|
CallerSkipFrame(1).
|
||||||
|
Str("event", "login").
|
||||||
|
Str("result", "success").
|
||||||
|
Str("username", username).
|
||||||
|
Str("provider", provider).
|
||||||
|
Str("ip", ip).
|
||||||
|
Send()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) AuditLoginFailure(username, provider, ip, reason string) {
|
||||||
|
l.audit.Warn().
|
||||||
|
CallerSkipFrame(1).
|
||||||
|
Str("event", "login").
|
||||||
|
Str("result", "failure").
|
||||||
|
Str("username", username).
|
||||||
|
Str("provider", provider).
|
||||||
|
Str("ip", ip).
|
||||||
|
Str("reason", reason).
|
||||||
|
Send()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) AuditLogout(username, provider, ip string) {
|
||||||
|
l.audit.Info().
|
||||||
|
CallerSkipFrame(1).
|
||||||
|
Str("event", "logout").
|
||||||
|
Str("result", "success").
|
||||||
|
Str("username", username).
|
||||||
|
Str("provider", provider).
|
||||||
|
Str("ip", ip).
|
||||||
|
Send()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used for testing
|
||||||
|
func (l *Logger) GetConfig() model.LogConfig {
|
||||||
|
return l.config
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
package logger_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLogger(t *testing.T) {
|
||||||
|
type testCase struct {
|
||||||
|
description string
|
||||||
|
run func(t *testing.T)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []testCase{
|
||||||
|
{
|
||||||
|
description: "Should create a simple logger with the expected config",
|
||||||
|
run: func(t *testing.T) {
|
||||||
|
l := logger.NewLogger().WithSimpleConfig()
|
||||||
|
l.Init()
|
||||||
|
|
||||||
|
cfg := l.GetConfig()
|
||||||
|
|
||||||
|
assert.Equal(t, cfg, model.LogConfig{
|
||||||
|
Level: "info",
|
||||||
|
Json: false,
|
||||||
|
Streams: model.LogStreams{
|
||||||
|
HTTP: model.LogStreamConfig{Enabled: true},
|
||||||
|
App: model.LogStreamConfig{Enabled: true},
|
||||||
|
Audit: model.LogStreamConfig{Enabled: false},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Should create a test logger with the expected config",
|
||||||
|
run: func(t *testing.T) {
|
||||||
|
l := logger.NewLogger().WithTestConfig()
|
||||||
|
l.Init()
|
||||||
|
|
||||||
|
cfg := l.GetConfig()
|
||||||
|
|
||||||
|
assert.Equal(t, cfg, model.LogConfig{
|
||||||
|
Level: "trace",
|
||||||
|
Json: false,
|
||||||
|
Streams: model.LogStreams{
|
||||||
|
HTTP: model.LogStreamConfig{Enabled: true},
|
||||||
|
App: model.LogStreamConfig{Enabled: true},
|
||||||
|
Audit: model.LogStreamConfig{Enabled: true},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Should create a logger with a custom config",
|
||||||
|
run: func(t *testing.T) {
|
||||||
|
customCfg := model.LogConfig{
|
||||||
|
Level: "debug",
|
||||||
|
Json: true,
|
||||||
|
Streams: model.LogStreams{
|
||||||
|
HTTP: model.LogStreamConfig{Enabled: false},
|
||||||
|
App: model.LogStreamConfig{Enabled: true},
|
||||||
|
Audit: model.LogStreamConfig{Enabled: false},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
l := logger.NewLogger().WithConfig(customCfg)
|
||||||
|
l.Init()
|
||||||
|
|
||||||
|
cfg := l.GetConfig()
|
||||||
|
|
||||||
|
assert.Equal(t, cfg, customCfg)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Default logger should use error type and log json",
|
||||||
|
run: func(t *testing.T) {
|
||||||
|
buf := bytes.Buffer{}
|
||||||
|
|
||||||
|
l := logger.NewLogger().WithWriter(&buf)
|
||||||
|
l.Init()
|
||||||
|
|
||||||
|
cfg := l.GetConfig()
|
||||||
|
|
||||||
|
assert.Equal(t, cfg, model.LogConfig{
|
||||||
|
Level: "error",
|
||||||
|
Json: true,
|
||||||
|
Streams: model.LogStreams{
|
||||||
|
HTTP: model.LogStreamConfig{Enabled: true},
|
||||||
|
App: model.LogStreamConfig{Enabled: true},
|
||||||
|
Audit: model.LogStreamConfig{Enabled: false},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
l.App.Error().Msg("test")
|
||||||
|
|
||||||
|
var entry map[string]any
|
||||||
|
err := json.Unmarshal(buf.Bytes(), &entry)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, "test", entry["message"])
|
||||||
|
assert.Equal(t, "app", entry["stream"])
|
||||||
|
assert.Equal(t, "error", entry["level"])
|
||||||
|
assert.NotEmpty(t, entry["time"])
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Should default to error level if an invalid level is provided",
|
||||||
|
run: func(t *testing.T) {
|
||||||
|
buf := bytes.Buffer{}
|
||||||
|
|
||||||
|
customCfg := model.LogConfig{
|
||||||
|
Level: "invalid",
|
||||||
|
Json: false,
|
||||||
|
Streams: model.LogStreams{
|
||||||
|
HTTP: model.LogStreamConfig{Enabled: true},
|
||||||
|
App: model.LogStreamConfig{Enabled: true},
|
||||||
|
Audit: model.LogStreamConfig{Enabled: false},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
l := logger.NewLogger().WithConfig(customCfg).WithWriter(&buf)
|
||||||
|
l.Init()
|
||||||
|
|
||||||
|
assert.Equal(t, zerolog.ErrorLevel, l.App.GetLevel())
|
||||||
|
assert.Equal(t, zerolog.ErrorLevel, l.HTTP.GetLevel())
|
||||||
|
|
||||||
|
// should not get logged
|
||||||
|
l.AuditLoginFailure("test", "test", "test", "test")
|
||||||
|
|
||||||
|
assert.Empty(t, buf.String())
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Should use nop logger for disabled streams",
|
||||||
|
run: func(t *testing.T) {
|
||||||
|
buf := bytes.Buffer{}
|
||||||
|
|
||||||
|
customCfg := model.LogConfig{
|
||||||
|
Level: "info",
|
||||||
|
Json: false,
|
||||||
|
Streams: model.LogStreams{
|
||||||
|
HTTP: model.LogStreamConfig{Enabled: false},
|
||||||
|
App: model.LogStreamConfig{Enabled: true},
|
||||||
|
Audit: model.LogStreamConfig{Enabled: false},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
l := logger.NewLogger().WithConfig(customCfg).WithWriter(&buf)
|
||||||
|
l.Init()
|
||||||
|
|
||||||
|
assert.Equal(t, zerolog.Disabled, l.HTTP.GetLevel())
|
||||||
|
|
||||||
|
l.App.Info().Msg("test")
|
||||||
|
|
||||||
|
l.AuditLoginFailure("test", "test", "test", "test")
|
||||||
|
|
||||||
|
assert.NotEmpty(t, buf.String())
|
||||||
|
assert.Equal(t, 119, buf.Len()) // it's the length of the test log entry
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.description, test.run)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user