first commit
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
)
|
||||
|
||||
func NewSessionLogger() (*log.Logger, *os.File, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("home dir: %w", err)
|
||||
}
|
||||
logDir := filepath.Join(home, ".config", "ai-agent", "logs")
|
||||
if err := os.MkdirAll(logDir, 0o755); err != nil {
|
||||
return nil, nil, fmt.Errorf("create log dir: %w", err)
|
||||
}
|
||||
filename := time.Now().Format("2006-01-02_15-04-05") + ".log"
|
||||
f, err := os.Create(filepath.Join(logDir, filename))
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("create log file: %w", err)
|
||||
}
|
||||
logger := log.NewWithOptions(f, log.Options{
|
||||
ReportTimestamp: true,
|
||||
TimeFormat: time.RFC3339,
|
||||
Prefix: "ai-agent",
|
||||
Level: log.DebugLevel,
|
||||
})
|
||||
return logger, f, nil
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewSessionLogger(t *testing.T) {
|
||||
logger, f, err := NewSessionLogger()
|
||||
if err != nil {
|
||||
t.Fatalf("NewSessionLogger() error: %v", err)
|
||||
}
|
||||
if f != nil {
|
||||
defer f.Close()
|
||||
defer os.Remove(f.Name())
|
||||
}
|
||||
if logger == nil {
|
||||
t.Fatal("logger should not be nil")
|
||||
}
|
||||
if f == nil {
|
||||
t.Fatal("file should not be nil")
|
||||
}
|
||||
dir := filepath.Dir(f.Name())
|
||||
if !strings.Contains(dir, filepath.Join(".config", "ai-agent", "logs")) {
|
||||
t.Errorf("log file should be in ~/.config/ai-agent/logs/, got %q", dir)
|
||||
}
|
||||
logger.Info("test message", "key", "value")
|
||||
}
|
||||
|
||||
func TestNilLoggerNoPanic(t *testing.T) {
|
||||
var called bool
|
||||
logger, f, err := NewSessionLogger()
|
||||
if err == nil && f != nil {
|
||||
defer f.Close()
|
||||
defer os.Remove(f.Name())
|
||||
logger.Info("test")
|
||||
called = true
|
||||
}
|
||||
_ = called
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
type LogEntry struct {
|
||||
Path string
|
||||
ModTime time.Time
|
||||
Size int64
|
||||
}
|
||||
|
||||
func LogDir() string {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return filepath.Join(home, ".config", "ai-agent", "logs")
|
||||
}
|
||||
|
||||
func ListLogs(n int) ([]LogEntry, error) {
|
||||
return listLogsIn(LogDir(), n)
|
||||
}
|
||||
|
||||
func listLogsIn(dir string, n int) ([]LogEntry, error) {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read log dir: %w", err)
|
||||
}
|
||||
var logs []LogEntry
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
continue
|
||||
}
|
||||
info, err := e.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
logs = append(logs, LogEntry{
|
||||
Path: filepath.Join(dir, e.Name()),
|
||||
ModTime: info.ModTime(),
|
||||
Size: info.Size(),
|
||||
})
|
||||
}
|
||||
sort.Slice(logs, func(i, j int) bool {
|
||||
return logs[i].ModTime.After(logs[j].ModTime)
|
||||
})
|
||||
if n > 0 && n < len(logs) {
|
||||
logs = logs[:n]
|
||||
}
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
func LatestLogPath() (string, error) {
|
||||
return latestLogPathIn(LogDir())
|
||||
}
|
||||
|
||||
func latestLogPathIn(dir string) (string, error) {
|
||||
logs, err := listLogsIn(dir, 1)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(logs) == 0 {
|
||||
return "", fmt.Errorf("no log files found in %s", dir)
|
||||
}
|
||||
return logs[0].Path, nil
|
||||
}
|
||||
|
||||
func TailLog(path string, n int) ([]string, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open log: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
var lines []string
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
lines = append(lines, scanner.Text())
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, fmt.Errorf("read log: %w", err)
|
||||
}
|
||||
if n > 0 && n < len(lines) {
|
||||
lines = lines[len(lines)-n:]
|
||||
}
|
||||
return lines, nil
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// helper creates n temp log files in dir with distinct mod times.
|
||||
func createFakeLogs(t *testing.T, dir string, n int) []string {
|
||||
t.Helper()
|
||||
var paths []string
|
||||
for i := range n {
|
||||
name := filepath.Join(dir, "2025-01-01_00-00-0"+string(rune('0'+i))+".log")
|
||||
if err := os.WriteFile(name, []byte("line "+string(rune('0'+i))+"\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Stagger mod times so ordering is deterministic.
|
||||
ts := time.Now().Add(time.Duration(i) * time.Second)
|
||||
if err := os.Chtimes(name, ts, ts); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
paths = append(paths, name)
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
func TestListLogs(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
createFakeLogs(t, dir, 5)
|
||||
|
||||
logs, err := listLogsIn(dir, 3)
|
||||
if err != nil {
|
||||
t.Fatalf("listLogsIn error: %v", err)
|
||||
}
|
||||
if len(logs) != 3 {
|
||||
t.Fatalf("expected 3 entries, got %d", len(logs))
|
||||
}
|
||||
|
||||
// Verify newest-first ordering.
|
||||
for i := 1; i < len(logs); i++ {
|
||||
if logs[i].ModTime.After(logs[i-1].ModTime) {
|
||||
t.Errorf("entry %d (%v) is newer than entry %d (%v)", i, logs[i].ModTime, i-1, logs[i-1].ModTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestListLogs_All(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
createFakeLogs(t, dir, 4)
|
||||
|
||||
logs, err := listLogsIn(dir, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("listLogsIn error: %v", err)
|
||||
}
|
||||
if len(logs) != 4 {
|
||||
t.Fatalf("expected 4 entries, got %d", len(logs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListLogs_EmptyDir(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
logs, err := listLogsIn(dir, 5)
|
||||
if err != nil {
|
||||
t.Fatalf("listLogsIn error: %v", err)
|
||||
}
|
||||
if len(logs) != 0 {
|
||||
t.Fatalf("expected 0 entries, got %d", len(logs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListLogs_MissingDir(t *testing.T) {
|
||||
_, err := listLogsIn("/tmp/nonexistent-log-dir-test-xyz", 5)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing dir")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTailLog(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "test.log")
|
||||
content := "line1\nline2\nline3\nline4\nline5\n"
|
||||
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
lines, err := TailLog(path, 3)
|
||||
if err != nil {
|
||||
t.Fatalf("TailLog error: %v", err)
|
||||
}
|
||||
if len(lines) != 3 {
|
||||
t.Fatalf("expected 3 lines, got %d", len(lines))
|
||||
}
|
||||
if lines[0] != "line3" {
|
||||
t.Errorf("expected 'line3', got %q", lines[0])
|
||||
}
|
||||
if lines[2] != "line5" {
|
||||
t.Errorf("expected 'line5', got %q", lines[2])
|
||||
}
|
||||
}
|
||||
|
||||
func TestTailLog_FewerLines(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "short.log")
|
||||
if err := os.WriteFile(path, []byte("only\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
lines, err := TailLog(path, 100)
|
||||
if err != nil {
|
||||
t.Fatalf("TailLog error: %v", err)
|
||||
}
|
||||
if len(lines) != 1 {
|
||||
t.Fatalf("expected 1 line, got %d", len(lines))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTailLog_MissingFile(t *testing.T) {
|
||||
_, err := TailLog("/tmp/nonexistent-file-test-xyz.log", 10)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLatestLogPath(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
paths := createFakeLogs(t, dir, 3)
|
||||
|
||||
latest, err := latestLogPathIn(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("latestLogPathIn error: %v", err)
|
||||
}
|
||||
// The last created file has the newest mod time.
|
||||
expected := paths[len(paths)-1]
|
||||
if latest != expected {
|
||||
t.Errorf("expected %q, got %q", expected, latest)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLatestLogPath_EmptyDir(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
_, err := latestLogPathIn(dir)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for empty dir")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogDir(t *testing.T) {
|
||||
dir := LogDir()
|
||||
if dir == "" {
|
||||
t.Fatal("LogDir should not be empty")
|
||||
}
|
||||
if filepath.Base(dir) != "logs" {
|
||||
t.Errorf("expected dir to end in 'logs', got %q", dir)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user