Skip to content

Commit 71a07ab

Browse files
committed
feat(logger): add structured logging with rotation
- add `logger.go` with json structured logging using `slog` - implement automatic log file rotation (max 3 files) - support custom log paths and xdg config directory - add `Load()`, `Debug()`, `Info()`, `Warn()`, `Error()` functions - add tests covering all logging functions and rotation logic - add default log path generation with timestamp naming
1 parent 4c145b7 commit 71a07ab

File tree

2 files changed

+469
-0
lines changed

2 files changed

+469
-0
lines changed

internal/logger/logger.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package logger
2+
3+
import (
4+
"fmt"
5+
"log/slog"
6+
"os"
7+
"path/filepath"
8+
"sort"
9+
"time"
10+
)
11+
12+
var Logger *slog.Logger
13+
14+
const maxLogFiles = 3
15+
16+
func Load(logPath string) error {
17+
var logFilePath string
18+
var err error
19+
20+
if logPath != "" {
21+
logFilePath = logPath
22+
} else {
23+
logFilePath, err = getDefaultLogPath()
24+
if err != nil {
25+
return fmt.Errorf("failed to get default log path: %w", err)
26+
}
27+
}
28+
29+
return initLogger(logFilePath)
30+
}
31+
32+
func initLogger(logPath string) error {
33+
logDir := filepath.Dir(logPath)
34+
if err := os.MkdirAll(logDir, 0755); err != nil {
35+
return fmt.Errorf("failed to create log directory: %w", err)
36+
}
37+
38+
defaultLogPath, err := getDefaultLogPath()
39+
if err == nil && filepath.Dir(defaultLogPath) == logDir {
40+
if err := rotateLogFiles(logDir); err != nil {
41+
return fmt.Errorf("failed to rotate log files: %w", err)
42+
}
43+
}
44+
45+
logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
46+
if err != nil {
47+
return fmt.Errorf("failed to open log file: %w", err)
48+
}
49+
50+
handler := slog.NewJSONHandler(logFile, &slog.HandlerOptions{
51+
Level: slog.LevelDebug,
52+
AddSource: true,
53+
})
54+
55+
Logger = slog.New(handler)
56+
57+
Info("Logger initialized", "log_file", logPath, "format", "json")
58+
return nil
59+
}
60+
61+
func rotateLogFiles(logDir string) error {
62+
pattern := filepath.Join(logDir, "*.kyma.log")
63+
matches, err := filepath.Glob(pattern)
64+
if err != nil {
65+
return fmt.Errorf("failed to find log files: %w", err)
66+
}
67+
68+
if len(matches) < maxLogFiles {
69+
return nil
70+
}
71+
72+
type logFile struct {
73+
path string
74+
modTime time.Time
75+
}
76+
77+
var logFiles []logFile
78+
for _, match := range matches {
79+
info, err := os.Stat(match)
80+
if err != nil {
81+
continue
82+
}
83+
logFiles = append(logFiles, logFile{
84+
path: match,
85+
modTime: info.ModTime(),
86+
})
87+
}
88+
89+
sort.Slice(logFiles, func(i, j int) bool {
90+
return logFiles[i].modTime.Before(logFiles[j].modTime)
91+
})
92+
93+
filesToRemove := len(logFiles) - (maxLogFiles - 1)
94+
for i := range filesToRemove {
95+
if err := os.Remove(logFiles[i].path); err != nil {
96+
fmt.Fprintf(os.Stderr, "Warning: failed to remove old log file %s: %v\n", logFiles[i].path, err)
97+
}
98+
}
99+
100+
return nil
101+
}
102+
103+
func getDefaultLogPath() (string, error) {
104+
home, err := os.UserHomeDir()
105+
if err != nil {
106+
return "", fmt.Errorf("failed to get user home directory: %w", err)
107+
}
108+
109+
// Check XDG_CONFIG_HOME first, then fall back to ~/.config
110+
xdgConfigHome := os.Getenv("XDG_CONFIG_HOME")
111+
var configDir string
112+
if xdgConfigHome != "" {
113+
configDir = filepath.Join(xdgConfigHome, "kyma")
114+
} else {
115+
configDir = filepath.Join(home, ".config", "kyma")
116+
}
117+
118+
logsDir := filepath.Join(configDir, "logs")
119+
timestamp := time.Now().Format("2006-01-02_15-04-05")
120+
logFile := fmt.Sprintf("%s.kyma.log", timestamp)
121+
122+
return filepath.Join(logsDir, logFile), nil
123+
}
124+
125+
func Debug(msg string, keyvals ...any) {
126+
if Logger != nil {
127+
Logger.Debug(msg, keyvals...)
128+
}
129+
}
130+
131+
func Info(msg string, keyvals ...any) {
132+
if Logger != nil {
133+
Logger.Info(msg, keyvals...)
134+
}
135+
}
136+
137+
func Warn(msg string, keyvals ...any) {
138+
if Logger != nil {
139+
Logger.Warn(msg, keyvals...)
140+
}
141+
}
142+
143+
func Error(msg string, keyvals ...any) {
144+
if Logger != nil {
145+
Logger.Error(msg, keyvals...)
146+
}
147+
}

0 commit comments

Comments
 (0)