* First commit
This commit is contained in:
239
pkg/logger/logger.go
Normal file
239
pkg/logger/logger.go
Normal file
@@ -0,0 +1,239 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type LogLevel int
|
||||
|
||||
const (
|
||||
DEBUG LogLevel = iota
|
||||
INFO
|
||||
WARN
|
||||
ERROR
|
||||
FATAL
|
||||
)
|
||||
|
||||
var (
|
||||
logLevelNames = map[LogLevel]string{
|
||||
DEBUG: "DEBUG",
|
||||
INFO: "INFO",
|
||||
WARN: "WARN",
|
||||
ERROR: "ERROR",
|
||||
FATAL: "FATAL",
|
||||
}
|
||||
|
||||
currentLevel = INFO
|
||||
logger *Logger
|
||||
once sync.Once
|
||||
mu sync.RWMutex
|
||||
)
|
||||
|
||||
type Logger struct {
|
||||
file *os.File
|
||||
}
|
||||
|
||||
type LogEntry struct {
|
||||
Level string `json:"level"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
Component string `json:"component,omitempty"`
|
||||
Message string `json:"message"`
|
||||
Fields map[string]interface{} `json:"fields,omitempty"`
|
||||
Caller string `json:"caller,omitempty"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
once.Do(func() {
|
||||
logger = &Logger{}
|
||||
})
|
||||
}
|
||||
|
||||
func SetLevel(level LogLevel) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
currentLevel = level
|
||||
}
|
||||
|
||||
func GetLevel() LogLevel {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
return currentLevel
|
||||
}
|
||||
|
||||
func EnableFileLogging(filePath string) error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
file, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open log file: %w", err)
|
||||
}
|
||||
|
||||
if logger.file != nil {
|
||||
logger.file.Close()
|
||||
}
|
||||
|
||||
logger.file = file
|
||||
log.Println("File logging enabled:", filePath)
|
||||
return nil
|
||||
}
|
||||
|
||||
func DisableFileLogging() {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
if logger.file != nil {
|
||||
logger.file.Close()
|
||||
logger.file = nil
|
||||
log.Println("File logging disabled")
|
||||
}
|
||||
}
|
||||
|
||||
func logMessage(level LogLevel, component string, message string, fields map[string]interface{}) {
|
||||
if level < currentLevel {
|
||||
return
|
||||
}
|
||||
|
||||
entry := LogEntry{
|
||||
Level: logLevelNames[level],
|
||||
Timestamp: time.Now().UTC().Format(time.RFC3339),
|
||||
Component: component,
|
||||
Message: message,
|
||||
Fields: fields,
|
||||
}
|
||||
|
||||
if pc, file, line, ok := runtime.Caller(2); ok {
|
||||
fn := runtime.FuncForPC(pc)
|
||||
if fn != nil {
|
||||
entry.Caller = fmt.Sprintf("%s:%d (%s)", file, line, fn.Name())
|
||||
}
|
||||
}
|
||||
|
||||
if logger.file != nil {
|
||||
jsonData, err := json.Marshal(entry)
|
||||
if err == nil {
|
||||
logger.file.WriteString(string(jsonData) + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
var fieldStr string
|
||||
if len(fields) > 0 {
|
||||
fieldStr = " " + formatFields(fields)
|
||||
}
|
||||
|
||||
logLine := fmt.Sprintf("[%s] [%s]%s %s%s",
|
||||
entry.Timestamp,
|
||||
logLevelNames[level],
|
||||
formatComponent(component),
|
||||
message,
|
||||
fieldStr,
|
||||
)
|
||||
|
||||
log.Println(logLine)
|
||||
|
||||
if level == FATAL {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func formatComponent(component string) string {
|
||||
if component == "" {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf(" %s:", component)
|
||||
}
|
||||
|
||||
func formatFields(fields map[string]interface{}) string {
|
||||
var parts []string
|
||||
for k, v := range fields {
|
||||
parts = append(parts, fmt.Sprintf("%s=%v", k, v))
|
||||
}
|
||||
return fmt.Sprintf("{%s}", strings.Join(parts, ", "))
|
||||
}
|
||||
|
||||
func Debug(message string) {
|
||||
logMessage(DEBUG, "", message, nil)
|
||||
}
|
||||
|
||||
func DebugC(component string, message string) {
|
||||
logMessage(DEBUG, component, message, nil)
|
||||
}
|
||||
|
||||
func DebugF(message string, fields map[string]interface{}) {
|
||||
logMessage(DEBUG, "", message, fields)
|
||||
}
|
||||
|
||||
func DebugCF(component string, message string, fields map[string]interface{}) {
|
||||
logMessage(DEBUG, component, message, fields)
|
||||
}
|
||||
|
||||
func Info(message string) {
|
||||
logMessage(INFO, "", message, nil)
|
||||
}
|
||||
|
||||
func InfoC(component string, message string) {
|
||||
logMessage(INFO, component, message, nil)
|
||||
}
|
||||
|
||||
func InfoF(message string, fields map[string]interface{}) {
|
||||
logMessage(INFO, "", message, fields)
|
||||
}
|
||||
|
||||
func InfoCF(component string, message string, fields map[string]interface{}) {
|
||||
logMessage(INFO, component, message, fields)
|
||||
}
|
||||
|
||||
func Warn(message string) {
|
||||
logMessage(WARN, "", message, nil)
|
||||
}
|
||||
|
||||
func WarnC(component string, message string) {
|
||||
logMessage(WARN, component, message, nil)
|
||||
}
|
||||
|
||||
func WarnF(message string, fields map[string]interface{}) {
|
||||
logMessage(WARN, "", message, fields)
|
||||
}
|
||||
|
||||
func WarnCF(component string, message string, fields map[string]interface{}) {
|
||||
logMessage(WARN, component, message, fields)
|
||||
}
|
||||
|
||||
func Error(message string) {
|
||||
logMessage(ERROR, "", message, nil)
|
||||
}
|
||||
|
||||
func ErrorC(component string, message string) {
|
||||
logMessage(ERROR, component, message, nil)
|
||||
}
|
||||
|
||||
func ErrorF(message string, fields map[string]interface{}) {
|
||||
logMessage(ERROR, "", message, fields)
|
||||
}
|
||||
|
||||
func ErrorCF(component string, message string, fields map[string]interface{}) {
|
||||
logMessage(ERROR, component, message, fields)
|
||||
}
|
||||
|
||||
func Fatal(message string) {
|
||||
logMessage(FATAL, "", message, nil)
|
||||
}
|
||||
|
||||
func FatalC(component string, message string) {
|
||||
logMessage(FATAL, component, message, nil)
|
||||
}
|
||||
|
||||
func FatalF(message string, fields map[string]interface{}) {
|
||||
logMessage(FATAL, "", message, fields)
|
||||
}
|
||||
|
||||
func FatalCF(component string, message string, fields map[string]interface{}) {
|
||||
logMessage(FATAL, component, message, fields)
|
||||
}
|
||||
139
pkg/logger/logger_test.go
Normal file
139
pkg/logger/logger_test.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLogLevelFiltering(t *testing.T) {
|
||||
initialLevel := GetLevel()
|
||||
defer SetLevel(initialLevel)
|
||||
|
||||
SetLevel(WARN)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
level LogLevel
|
||||
shouldLog bool
|
||||
}{
|
||||
{"DEBUG message", DEBUG, false},
|
||||
{"INFO message", INFO, false},
|
||||
{"WARN message", WARN, true},
|
||||
{"ERROR message", ERROR, true},
|
||||
{"FATAL message", FATAL, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
switch tt.level {
|
||||
case DEBUG:
|
||||
Debug(tt.name)
|
||||
case INFO:
|
||||
Info(tt.name)
|
||||
case WARN:
|
||||
Warn(tt.name)
|
||||
case ERROR:
|
||||
Error(tt.name)
|
||||
case FATAL:
|
||||
if tt.shouldLog {
|
||||
t.Logf("FATAL test skipped to prevent program exit")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
SetLevel(INFO)
|
||||
}
|
||||
|
||||
func TestLoggerWithComponent(t *testing.T) {
|
||||
initialLevel := GetLevel()
|
||||
defer SetLevel(initialLevel)
|
||||
|
||||
SetLevel(DEBUG)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
component string
|
||||
message string
|
||||
fields map[string]interface{}
|
||||
}{
|
||||
{"Simple message", "test", "Hello, world!", nil},
|
||||
{"Message with component", "discord", "Discord message", nil},
|
||||
{"Message with fields", "telegram", "Telegram message", map[string]interface{}{
|
||||
"user_id": "12345",
|
||||
"count": 42,
|
||||
}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
switch {
|
||||
case tt.fields == nil && tt.component != "":
|
||||
InfoC(tt.component, tt.message)
|
||||
case tt.fields != nil:
|
||||
InfoF(tt.message, tt.fields)
|
||||
default:
|
||||
Info(tt.message)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
SetLevel(INFO)
|
||||
}
|
||||
|
||||
func TestLogLevels(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
level LogLevel
|
||||
want string
|
||||
}{
|
||||
{"DEBUG level", DEBUG, "DEBUG"},
|
||||
{"INFO level", INFO, "INFO"},
|
||||
{"WARN level", WARN, "WARN"},
|
||||
{"ERROR level", ERROR, "ERROR"},
|
||||
{"FATAL level", FATAL, "FATAL"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if logLevelNames[tt.level] != tt.want {
|
||||
t.Errorf("logLevelNames[%d] = %s, want %s", tt.level, logLevelNames[tt.level], tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetGetLevel(t *testing.T) {
|
||||
initialLevel := GetLevel()
|
||||
defer SetLevel(initialLevel)
|
||||
|
||||
tests := []LogLevel{DEBUG, INFO, WARN, ERROR, FATAL}
|
||||
|
||||
for _, level := range tests {
|
||||
SetLevel(level)
|
||||
if GetLevel() != level {
|
||||
t.Errorf("SetLevel(%v) -> GetLevel() = %v, want %v", level, GetLevel(), level)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoggerHelperFunctions(t *testing.T) {
|
||||
initialLevel := GetLevel()
|
||||
defer SetLevel(initialLevel)
|
||||
|
||||
SetLevel(INFO)
|
||||
|
||||
Debug("This should not log")
|
||||
Info("This should log")
|
||||
Warn("This should log")
|
||||
Error("This should log")
|
||||
|
||||
InfoC("test", "Component message")
|
||||
InfoF("Fields message", map[string]interface{}{"key": "value"})
|
||||
|
||||
WarnC("test", "Warning with component")
|
||||
ErrorF("Error with fields", map[string]interface{}{"error": "test"})
|
||||
|
||||
SetLevel(DEBUG)
|
||||
DebugC("test", "Debug with component")
|
||||
WarnF("Warning with fields", map[string]interface{}{"key": "value"})
|
||||
}
|
||||
Reference in New Issue
Block a user