Add AGENTS.md, simplify code, 2 fixes in log package, 3 bugs fixed.

- Add AGENTS.md for vibecoding ai data.
- Remove github.com/pkg/errors dependency
- writeWithContext — remove goroutine overhead, just check context before fn
- copyWithContext — remove unnecessary panic recovery
- Remove panic recovery tests (TestWriteWithContextPanic, TestCopyWithContextPanic, panicReader)
- Error, Warn, Info, Debug, Fatal — accept ...any instead of string (enables log.Error(err))
- Close — don't panic if Log is nil, don't close stderr
- Add unit tests (internal/log/main_test.go)
- getClientIP — trim spaces from X-Forwarded-For, use net.SplitHostPort for IPv6
- isExcluded — normalize paths to forward slashes for cross-platform regex matching
- writeWithContext godoc — fix stale comment about panic recovery
- internal/log/main_test.go — log package tests (19 tests)
This commit is contained in:
eleksir
2026-03-27 01:24:32 +03:00
parent 0bea45b770
commit 16ae887196
11 changed files with 681 additions and 98 deletions

View File

@@ -82,9 +82,9 @@ func Init(level, filename string) error {
return err
}
// Error passes message directly to [slog.Error()].
func Error(message string) {
slog.Error(message)
// Error logs args at error level using [slog.Error()].
func Error(args ...any) {
slog.Error(fmt.Sprint(args...))
}
// Errorf logs message on error log level and allows to supply arguments in printf() style.
@@ -92,9 +92,9 @@ func Errorf(format string, a ...any) {
slog.Error(fmt.Sprintf(format, a...))
}
// Warn passes message directly to [slog.Warn()].
func Warn(message string) {
slog.Warn(message)
// Warn logs args at warn level using [slog.Warn()].
func Warn(args ...any) {
slog.Warn(fmt.Sprint(args...))
}
// Warnf logs message on warn log level and allows to supply arguments in printf() style.
@@ -102,9 +102,9 @@ func Warnf(format string, a ...any) {
slog.Warn(fmt.Sprintf(format, a...))
}
// Info passes message directly to [slog.Info()].
func Info(message string) {
slog.Info(message)
// Info logs args at info level using [slog.Info()].
func Info(args ...any) {
slog.Info(fmt.Sprint(args...))
}
// Infof logs message on info log level and allows to supply arguments in printf() style.
@@ -112,9 +112,9 @@ func Infof(format string, a ...any) {
slog.Info(fmt.Sprintf(format, a...))
}
// Debug passes message directly to [slog.Debug()].
func Debug(message string) {
slog.Debug(message)
// Debug logs args at debug level using [slog.Debug()].
func Debug(args ...any) {
slog.Debug(fmt.Sprint(args...))
}
// Debugf logs message on debug log level and allows to supply arguments in printf() style.
@@ -122,9 +122,9 @@ func Debugf(format string, a ...any) {
slog.Debug(fmt.Sprintf(format, a...))
}
// Fatal passes message directly to [slog.Error()] and quits with status code 1.
func Fatal(message string) {
slog.Error(message)
// Fatal logs args at error level and quits with status code 1.
func Fatal(args ...any) {
slog.Error(fmt.Sprint(args...))
Close()
os.Exit(1)
}
@@ -139,6 +139,10 @@ func Fatalf(format string, a ...any) {
// Close closes log file descriptor.
func Close() {
if Log == nil || Log == os.Stderr {
return
}
if err := Log.Close(); err != nil {
msg := fmt.Sprintln("Failed to close log file:", err)
slog.Error(msg)

357
internal/log/main_test.go Normal file
View File

@@ -0,0 +1,357 @@
package log
import (
"errors"
"os"
"strings"
"testing"
)
// logFile holds the path to the current test log file for reading.
var logFilePath string
// initTestLog sets up a temp log file and returns cleanup function.
func initTestLog(t *testing.T, level string) func() {
t.Helper()
tmpFile, err := os.CreateTemp(t.TempDir(), "test_log_*.log")
if err != nil {
t.Fatal(err)
}
logFilePath = tmpFile.Name()
tmpFile.Close()
if err := Init(level, logFilePath); err != nil {
t.Fatal(err)
}
return func() {
Close()
logFilePath = ""
}
}
// getLogContent reads the log file and returns its contents.
func getLogContent(t *testing.T) string {
t.Helper()
if logFilePath == "" {
t.Fatal("logFilePath not set")
}
data, err := os.ReadFile(logFilePath)
if err != nil {
t.Fatal(err)
}
return string(data)
}
// TestInitDefaultLevel tests Init with empty level defaults to info.
func TestInitDefaultLevel(t *testing.T) {
cleanup := initTestLog(t, "")
defer cleanup()
Info("info message")
Debug("debug message")
content := getLogContent(t)
if !strings.Contains(content, "info message") {
t.Error("Expected info message in log")
}
if strings.Contains(content, "debug message") {
t.Error("Debug message should not appear at info level")
}
}
// TestInitErrorLevel tests Init with error level.
func TestInitErrorLevel(t *testing.T) {
cleanup := initTestLog(t, "error")
defer cleanup()
Error("error msg")
Warn("warn msg")
Info("info msg")
content := getLogContent(t)
if !strings.Contains(content, "error msg") {
t.Error("Expected error message in log")
}
if strings.Contains(content, "warn msg") {
t.Error("Warn message should not appear at error level")
}
if strings.Contains(content, "info msg") {
t.Error("Info message should not appear at error level")
}
}
// TestInitWarnLevel tests Init with warn level.
func TestInitWarnLevel(t *testing.T) {
cleanup := initTestLog(t, "warn")
defer cleanup()
Error("error msg")
Warn("warn msg")
Info("info msg")
content := getLogContent(t)
if !strings.Contains(content, "error msg") {
t.Error("Expected error message in log")
}
if !strings.Contains(content, "warn msg") {
t.Error("Expected warn message in log")
}
if strings.Contains(content, "info msg") {
t.Error("Info message should not appear at warn level")
}
}
// TestInitDebugLevel tests Init with debug level shows everything.
func TestInitDebugLevel(t *testing.T) {
cleanup := initTestLog(t, "debug")
defer cleanup()
Error("error msg")
Warn("warn msg")
Info("info msg")
Debug("debug msg")
content := getLogContent(t)
if !strings.Contains(content, "error msg") {
t.Error("Expected error message in log")
}
if !strings.Contains(content, "warn msg") {
t.Error("Expected warn message in log")
}
if !strings.Contains(content, "info msg") {
t.Error("Expected info message in log")
}
if !strings.Contains(content, "debug msg") {
t.Error("Expected debug message in log")
}
}
// TestInitStderr tests Init with empty filename uses stderr.
func TestInitStderr(t *testing.T) {
err := Init("info", "")
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if Log != os.Stderr {
t.Error("Expected Log to be os.Stderr")
}
}
// TestInitInvalidPath tests Init with invalid file path returns error.
func TestInitInvalidPath(t *testing.T) {
err := Init("info", "/nonexistent/path/to/log.log")
if err == nil {
t.Error("Expected error for invalid path, got nil")
}
}
// TestErrorWithString tests Error with a string argument.
func TestErrorWithString(t *testing.T) {
cleanup := initTestLog(t, "error")
defer cleanup()
Error("test error message")
content := getLogContent(t)
if !strings.Contains(content, "test error message") {
t.Error("Expected error message in log")
}
}
// TestErrorWithError tests Error with an error argument.
func TestErrorWithError(t *testing.T) {
cleanup := initTestLog(t, "error")
defer cleanup()
err := errors.New("something went wrong")
Error(err)
content := getLogContent(t)
if !strings.Contains(content, "something went wrong") {
t.Error("Expected error message in log")
}
}
// TestErrorWithMultipleArgs tests Error with multiple arguments.
func TestErrorWithMultipleArgs(t *testing.T) {
cleanup := initTestLog(t, "error")
defer cleanup()
err := errors.New("disk full")
Error("Failed to write: ", err)
content := getLogContent(t)
if !strings.Contains(content, "Failed to write:") {
t.Error("Expected prefix in log")
}
if !strings.Contains(content, "disk full") {
t.Error("Expected error message in log")
}
}
// TestWarnWithString tests Warn with a string argument.
func TestWarnWithString(t *testing.T) {
cleanup := initTestLog(t, "warn")
defer cleanup()
Warn("test warning")
content := getLogContent(t)
if !strings.Contains(content, "test warning") {
t.Error("Expected warning message in log")
}
}
// TestInfoWithString tests Info with a string argument.
func TestInfoWithString(t *testing.T) {
cleanup := initTestLog(t, "info")
defer cleanup()
Info("test info")
content := getLogContent(t)
if !strings.Contains(content, "test info") {
t.Error("Expected info message in log")
}
}
// TestDebugWithString tests Debug with a string argument.
func TestDebugWithString(t *testing.T) {
cleanup := initTestLog(t, "debug")
defer cleanup()
Debug("test debug")
content := getLogContent(t)
if !strings.Contains(content, "test debug") {
t.Error("Expected debug message in log")
}
}
// TestErrorf tests Errorf with format string.
func TestErrorf(t *testing.T) {
cleanup := initTestLog(t, "error")
defer cleanup()
Errorf("Failed to read %s: %v", "config.json", errors.New("not found"))
content := getLogContent(t)
if !strings.Contains(content, "Failed to read config.json: not found") {
t.Errorf("Expected formatted error in log, got: %s", content)
}
}
// TestWarnf tests Warnf with format string.
func TestWarnf(t *testing.T) {
cleanup := initTestLog(t, "warn")
defer cleanup()
Warnf("Skipping %s: %d files", "dir", 5)
content := getLogContent(t)
if !strings.Contains(content, "Skipping dir: 5 files") {
t.Errorf("Expected formatted warning in log, got: %s", content)
}
}
// TestInfof tests Infof with format string.
func TestInfof(t *testing.T) {
cleanup := initTestLog(t, "info")
defer cleanup()
Infof("Server started on %s:%d", "0.0.0.0", 8086)
content := getLogContent(t)
if !strings.Contains(content, "Server started on 0.0.0.0:8086") {
t.Errorf("Expected formatted info in log, got: %s", content)
}
}
// TestDebugf tests Debugf with format string.
func TestDebugf(t *testing.T) {
cleanup := initTestLog(t, "debug")
defer cleanup()
Debugf("Processing file %s (%d bytes)", "test.txt", 1024)
content := getLogContent(t)
if !strings.Contains(content, "Processing file test.txt (1024 bytes)") {
t.Errorf("Expected formatted debug in log, got: %s", content)
}
}
// TestClose tests that Close closes the log file.
func TestClose(t *testing.T) {
tmpFile, err := os.CreateTemp(t.TempDir(), "test_log_*.log")
if err != nil {
t.Fatal(err)
}
logFilePath = tmpFile.Name()
tmpFile.Close()
if err := Init("info", logFilePath); err != nil {
t.Fatal(err)
}
Close()
// Verify file is closed by trying to write (should fail).
// But we can't easily test this without reopening.
// Just verify no panic occurred.
logFilePath = ""
}
// TestCloseNil tests that Close doesn't panic when Log is nil.
func TestCloseNil(t *testing.T) {
Log = nil
// Should not panic.
Close()
}
// TestCloseStderr tests that Close doesn't close stderr.
func TestCloseStderr(t *testing.T) {
Log = os.Stderr
// Should not panic and should not close stderr.
Close()
// Verify stderr is still usable.
_, err := os.Stderr.WriteString("")
if err != nil {
t.Error("stderr should still be usable after Close()")
}
}
/* vim: setlocal ft=go noet ai ts=4 sw=4 sts=4: */