package app import ( "database/sql" "encoding/json" "fmt" "os" "path/filepath" "sync" "time" _ "github.com/mattn/go-sqlite3" ) // Record represents a captured event type Record struct { Timestamp time.Time `json:"timestamp"` RemoteAddr string `json:"remote_addr"` RemotePort string `json:"remote_port"` Service string `json:"service"` Details map[string]string `json:"details,omitempty"` RawPayload string `json:"raw_payload,omitempty"` } // Logger handles output to file / stdout / sqlite type Logger struct { mode string mu sync.Mutex f *os.File db *sql.DB } // NewLogger creates and initializes a Logger func NewLogger(cfg Config) (*Logger, error) { l := &Logger{mode: cfg.LogMode} switch cfg.LogMode { case "stdout": // nothing to open case "sqlite": if err := os.MkdirAll(filepath.Dir(cfg.LogPath), 0755); err != nil { return nil, fmt.Errorf("create db dir: %w", err) } db, err := sql.Open("sqlite3", cfg.LogPath) if err != nil { return nil, fmt.Errorf("open sqlite: %w", err) } l.db = db if err := l.ensureSQLiteSchema(); err != nil { return nil, err } default: // file if err := os.MkdirAll(filepath.Dir(cfg.LogPath), 0700); err != nil { return nil, fmt.Errorf("create log dir: %w", err) } f, err := os.OpenFile(cfg.LogPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600) if err != nil { return nil, fmt.Errorf("open log file: %w", err) } l.f = f } return l, nil } func (l *Logger) ensureSQLiteSchema() error { if l.db == nil { return nil } q := `CREATE TABLE IF NOT EXISTS logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp TEXT, remote_addr TEXT, remote_port TEXT, service TEXT, details TEXT, raw_payload TEXT )` _, err := l.db.Exec(q) return err } // Close closes any underlying resources func (l *Logger) Close() error { l.mu.Lock() defer l.mu.Unlock() if l.f != nil { _ = l.f.Sync() if err := l.f.Close(); err != nil { return err } } if l.db != nil { return l.db.Close() } return nil } // Log writes a Record according to the configured backend func (l *Logger) Log(r Record) error { l.mu.Lock() defer l.mu.Unlock() switch l.mode { case "stdout": b, _ := json.Marshal(r) fmt.Println(string(b)) return nil case "sqlite": if l.db == nil { return fmt.Errorf("sqlite DB not open") } detailsB, _ := json.Marshal(r.Details) _, err := l.db.Exec(`INSERT INTO logs (timestamp, remote_addr, remote_port, service, details, raw_payload) VALUES (?, ?, ?, ?, ?, ?)`, r.Timestamp.UTC().Format(time.RFC3339Nano), r.RemoteAddr, r.RemotePort, r.Service, string(detailsB), r.RawPayload) return err default: // file if l.f == nil { return fmt.Errorf("file logger not initialized") } enc := json.NewEncoder(l.f) if err := enc.Encode(r); err != nil { return err } return l.f.Sync() } }