124 lines
2.9 KiB
Go
124 lines
2.9 KiB
Go
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()
|
|
}
|
|
}
|