feat: add logger finally...
All checks were successful
Go Tests / Run Tests (1.24.2) (push) Successful in 9m48s

This commit is contained in:
Jeremy Tregunna 2025-04-23 02:28:13 -06:00
parent 139c33533e
commit a8c72fccfa
Signed by: jer
GPG Key ID: 1278B36BA6F5D5E4
2 changed files with 399 additions and 0 deletions

267
pkg/common/log/logger.go Normal file
View File

@ -0,0 +1,267 @@
// Package log provides a common logging interface for Kevo components.
package log
import (
"fmt"
"io"
"os"
"sync"
"time"
)
// Level represents the logging level
type Level int
const (
// LevelDebug level for detailed troubleshooting information
LevelDebug Level = iota
// LevelInfo level for general operational information
LevelInfo
// LevelWarn level for potentially harmful situations
LevelWarn
// LevelError level for error events that might still allow the application to continue
LevelError
// LevelFatal level for severe error events that will lead the application to abort
LevelFatal
)
// String returns the string representation of the log level
func (l Level) String() string {
switch l {
case LevelDebug:
return "DEBUG"
case LevelInfo:
return "INFO"
case LevelWarn:
return "WARN"
case LevelError:
return "ERROR"
case LevelFatal:
return "FATAL"
default:
return fmt.Sprintf("LEVEL(%d)", l)
}
}
// Logger interface defines the methods for logging at different levels
type Logger interface {
// Debug logs a debug-level message
Debug(msg string, args ...interface{})
// Info logs an info-level message
Info(msg string, args ...interface{})
// Warn logs a warning-level message
Warn(msg string, args ...interface{})
// Error logs an error-level message
Error(msg string, args ...interface{})
// Fatal logs a fatal-level message and then calls os.Exit(1)
Fatal(msg string, args ...interface{})
// WithFields returns a new logger with the given fields added to the context
WithFields(fields map[string]interface{}) Logger
// WithField returns a new logger with the given field added to the context
WithField(key string, value interface{}) Logger
// GetLevel returns the current logging level
GetLevel() Level
// SetLevel sets the logging level
SetLevel(level Level)
}
// StandardLogger implements the Logger interface with a standard output format
type StandardLogger struct {
mu sync.Mutex
level Level
out io.Writer
fields map[string]interface{}
}
// NewStandardLogger creates a new StandardLogger with the given options
func NewStandardLogger(options ...LoggerOption) *StandardLogger {
logger := &StandardLogger{
level: LevelInfo, // Default level
out: os.Stdout,
fields: make(map[string]interface{}),
}
// Apply options
for _, option := range options {
option(logger)
}
return logger
}
// LoggerOption is a function that configures a StandardLogger
type LoggerOption func(*StandardLogger)
// WithLevel sets the logging level
func WithLevel(level Level) LoggerOption {
return func(l *StandardLogger) {
l.level = level
}
}
// WithOutput sets the output writer
func WithOutput(out io.Writer) LoggerOption {
return func(l *StandardLogger) {
l.out = out
}
}
// WithInitialFields sets initial fields for the logger
func WithInitialFields(fields map[string]interface{}) LoggerOption {
return func(l *StandardLogger) {
for k, v := range fields {
l.fields[k] = v
}
}
}
// log logs a message at the specified level
func (l *StandardLogger) log(level Level, msg string, args ...interface{}) {
if level < l.level {
return
}
l.mu.Lock()
defer l.mu.Unlock()
// Format the message
formattedMsg := msg
if len(args) > 0 {
formattedMsg = fmt.Sprintf(msg, args...)
}
// Format timestamp
timestamp := time.Now().Format("2006-01-02 15:04:05.000")
// Format fields
fieldsStr := ""
if len(l.fields) > 0 {
for k, v := range l.fields {
fieldsStr += fmt.Sprintf(" %s=%v", k, v)
}
}
// Write the log entry
fmt.Fprintf(l.out, "[%s] [%s]%s %s\n", timestamp, level.String(), fieldsStr, formattedMsg)
// Exit if fatal
if level == LevelFatal {
os.Exit(1)
}
}
// Debug logs a debug-level message
func (l *StandardLogger) Debug(msg string, args ...interface{}) {
l.log(LevelDebug, msg, args...)
}
// Info logs an info-level message
func (l *StandardLogger) Info(msg string, args ...interface{}) {
l.log(LevelInfo, msg, args...)
}
// Warn logs a warning-level message
func (l *StandardLogger) Warn(msg string, args ...interface{}) {
l.log(LevelWarn, msg, args...)
}
// Error logs an error-level message
func (l *StandardLogger) Error(msg string, args ...interface{}) {
l.log(LevelError, msg, args...)
}
// Fatal logs a fatal-level message and then calls os.Exit(1)
func (l *StandardLogger) Fatal(msg string, args ...interface{}) {
l.log(LevelFatal, msg, args...)
}
// WithFields returns a new logger with the given fields added to the context
func (l *StandardLogger) WithFields(fields map[string]interface{}) Logger {
newLogger := &StandardLogger{
level: l.level,
out: l.out,
fields: make(map[string]interface{}, len(l.fields)+len(fields)),
}
// Copy existing fields
for k, v := range l.fields {
newLogger.fields[k] = v
}
// Add new fields
for k, v := range fields {
newLogger.fields[k] = v
}
return newLogger
}
// WithField returns a new logger with the given field added to the context
func (l *StandardLogger) WithField(key string, value interface{}) Logger {
return l.WithFields(map[string]interface{}{key: value})
}
// GetLevel returns the current logging level
func (l *StandardLogger) GetLevel() Level {
return l.level
}
// SetLevel sets the logging level
func (l *StandardLogger) SetLevel(level Level) {
l.level = level
}
// Default logger instance
var defaultLogger = NewStandardLogger()
// SetDefaultLogger sets the default logger instance
func SetDefaultLogger(logger *StandardLogger) {
defaultLogger = logger
}
// GetDefaultLogger returns the default logger instance
func GetDefaultLogger() *StandardLogger {
return defaultLogger
}
// These functions use the default logger
// Debug logs a debug-level message to the default logger
func Debug(msg string, args ...interface{}) {
defaultLogger.Debug(msg, args...)
}
// Info logs an info-level message to the default logger
func Info(msg string, args ...interface{}) {
defaultLogger.Info(msg, args...)
}
// Warn logs a warning-level message to the default logger
func Warn(msg string, args ...interface{}) {
defaultLogger.Warn(msg, args...)
}
// Error logs an error-level message to the default logger
func Error(msg string, args ...interface{}) {
defaultLogger.Error(msg, args...)
}
// Fatal logs a fatal-level message to the default logger and then calls os.Exit(1)
func Fatal(msg string, args ...interface{}) {
defaultLogger.Fatal(msg, args...)
}
// WithFields returns a new logger with the given fields added to the context
func WithFields(fields map[string]interface{}) Logger {
return defaultLogger.WithFields(fields)
}
// WithField returns a new logger with the given field added to the context
func WithField(key string, value interface{}) Logger {
return defaultLogger.WithField(key, value)
}
// SetLevel sets the logging level of the default logger
func SetLevel(level Level) {
defaultLogger.SetLevel(level)
}

View File

@ -0,0 +1,132 @@
package log
import (
"bytes"
"strings"
"testing"
)
func TestStandardLogger(t *testing.T) {
// Create a buffer to capture output
var buf bytes.Buffer
// Create a logger with the buffer as output
logger := NewStandardLogger(
WithOutput(&buf),
WithLevel(LevelDebug),
)
// Test debug level
logger.Debug("This is a debug message")
if !strings.Contains(buf.String(), "[DEBUG]") || !strings.Contains(buf.String(), "This is a debug message") {
t.Errorf("Debug logging failed, got: %s", buf.String())
}
buf.Reset()
// Test info level
logger.Info("This is an info message")
if !strings.Contains(buf.String(), "[INFO]") || !strings.Contains(buf.String(), "This is an info message") {
t.Errorf("Info logging failed, got: %s", buf.String())
}
buf.Reset()
// Test warn level
logger.Warn("This is a warning message")
if !strings.Contains(buf.String(), "[WARN]") || !strings.Contains(buf.String(), "This is a warning message") {
t.Errorf("Warn logging failed, got: %s", buf.String())
}
buf.Reset()
// Test error level
logger.Error("This is an error message")
if !strings.Contains(buf.String(), "[ERROR]") || !strings.Contains(buf.String(), "This is an error message") {
t.Errorf("Error logging failed, got: %s", buf.String())
}
buf.Reset()
// Test with fields
loggerWithFields := logger.WithFields(map[string]interface{}{
"component": "test",
"count": 123,
})
loggerWithFields.Info("Message with fields")
output := buf.String()
if !strings.Contains(output, "[INFO]") ||
!strings.Contains(output, "Message with fields") ||
!strings.Contains(output, "component=test") ||
!strings.Contains(output, "count=123") {
t.Errorf("Logging with fields failed, got: %s", output)
}
buf.Reset()
// Test with a single field
loggerWithField := logger.WithField("module", "logger")
loggerWithField.Info("Message with a field")
output = buf.String()
if !strings.Contains(output, "[INFO]") ||
!strings.Contains(output, "Message with a field") ||
!strings.Contains(output, "module=logger") {
t.Errorf("Logging with a field failed, got: %s", output)
}
buf.Reset()
// Test level filtering
logger.SetLevel(LevelError)
logger.Debug("This debug message should not appear")
logger.Info("This info message should not appear")
logger.Warn("This warning message should not appear")
logger.Error("This error message should appear")
output = buf.String()
if strings.Contains(output, "should not appear") ||
!strings.Contains(output, "This error message should appear") {
t.Errorf("Level filtering failed, got: %s", output)
}
buf.Reset()
// Test formatted messages
logger.SetLevel(LevelInfo)
logger.Info("Formatted %s with %d params", "message", 2)
if !strings.Contains(buf.String(), "Formatted message with 2 params") {
t.Errorf("Formatted message failed, got: %s", buf.String())
}
buf.Reset()
// Test GetLevel
if logger.GetLevel() != LevelInfo {
t.Errorf("GetLevel failed, expected LevelInfo, got: %v", logger.GetLevel())
}
}
func TestDefaultLogger(t *testing.T) {
// Save original default logger
originalLogger := defaultLogger
defer func() {
defaultLogger = originalLogger
}()
// Create a buffer to capture output
var buf bytes.Buffer
// Set a new default logger
SetDefaultLogger(NewStandardLogger(
WithOutput(&buf),
WithLevel(LevelInfo),
))
// Test global functions
Info("Global info message")
if !strings.Contains(buf.String(), "[INFO]") || !strings.Contains(buf.String(), "Global info message") {
t.Errorf("Global info logging failed, got: %s", buf.String())
}
buf.Reset()
// Test global with fields
WithField("global", true).Info("Global with field")
output := buf.String()
if !strings.Contains(output, "[INFO]") ||
!strings.Contains(output, "Global with field") ||
!strings.Contains(output, "global=true") {
t.Errorf("Global logging with field failed, got: %s", output)
}
buf.Reset()
}