Implement IP API daemon with GeoIP database support

- Create modular architecture with separate packages for config, database, HTTP, logging, and state management
- Implement Cobra CLI with daemon command
- Set up Uber FX dependency injection
- Add Chi router with health check and IP lookup endpoints
- Implement GeoIP database downloader with automatic updates
- Add state persistence for tracking database download times
- Include comprehensive test coverage for all components
- Configure structured logging with slog
- Add Makefile with test, lint, and build targets
- Support both IPv4 and IPv6 lookups
- Return country, city, ASN, and location data in JSON format
This commit is contained in:
2025-07-27 18:15:38 +02:00
commit 2a1710cca8
24 changed files with 2402 additions and 0 deletions

49
internal/log/log.go Normal file
View File

@@ -0,0 +1,49 @@
// Package log provides structured logging functionality.
package log
import (
"log/slog"
"os"
"strings"
"git.eeqj.de/sneak/ipapi/internal/config"
)
// New creates a new logger instance based on configuration.
func New(cfg *config.Config) *slog.Logger {
var level slog.Level
switch strings.ToLower(cfg.LogLevel) {
case "debug":
level = slog.LevelDebug
case "info":
level = slog.LevelInfo
case "warn", "warning":
level = slog.LevelWarn
case "error":
level = slog.LevelError
default:
level = slog.LevelInfo
}
opts := &slog.HandlerOptions{
Level: level,
}
var handler slog.Handler
if isTerminal() {
handler = slog.NewTextHandler(os.Stdout, opts)
} else {
handler = slog.NewJSONHandler(os.Stdout, opts)
}
return slog.New(handler)
}
func isTerminal() bool {
fileInfo, err := os.Stdout.Stat()
if err != nil {
return false
}
return (fileInfo.Mode() & os.ModeCharDevice) != 0
}

38
internal/log/log_test.go Normal file
View File

@@ -0,0 +1,38 @@
package log
import (
"testing"
"git.eeqj.de/sneak/ipapi/internal/config"
)
func TestNew(t *testing.T) {
tests := []struct {
name string
logLevel string
}{
{"debug level", "debug"},
{"info level", "info"},
{"warn level", "warn"},
{"warning level", "warning"},
{"error level", "error"},
{"invalid level", "invalid"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &config.Config{
LogLevel: tt.logLevel,
}
logger := New(cfg)
if logger == nil {
t.Fatal("expected logger, got nil")
}
})
}
}
func TestIsTerminal(t *testing.T) {
// Just test that it doesn't panic
_ = isTerminal()
}