Event logging
This commit is contained in:
@@ -7,12 +7,13 @@ import (
|
||||
|
||||
// Config represents the application configuration
|
||||
type Config struct {
|
||||
Server ServerConfig `json:"server"`
|
||||
WhatsApp []WhatsAppConfig `json:"whatsapp"`
|
||||
Hooks []Hook `json:"hooks"`
|
||||
Database DatabaseConfig `json:"database,omitempty"`
|
||||
Media MediaConfig `json:"media"`
|
||||
LogLevel string `json:"log_level"`
|
||||
Server ServerConfig `json:"server"`
|
||||
WhatsApp []WhatsAppConfig `json:"whatsapp"`
|
||||
Hooks []Hook `json:"hooks"`
|
||||
Database DatabaseConfig `json:"database,omitempty"`
|
||||
Media MediaConfig `json:"media"`
|
||||
EventLogger EventLoggerConfig `json:"event_logger,omitempty"`
|
||||
LogLevel string `json:"log_level"`
|
||||
}
|
||||
|
||||
// ServerConfig holds server-specific configuration
|
||||
@@ -47,12 +48,13 @@ type Hook struct {
|
||||
|
||||
// DatabaseConfig holds database connection information
|
||||
type DatabaseConfig struct {
|
||||
Type string `json:"type"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Database string `json:"database"`
|
||||
Type string `json:"type"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Database string `json:"database"`
|
||||
SQLitePath string `json:"sqlite_path,omitempty"` // Path to SQLite database file
|
||||
}
|
||||
|
||||
// MediaConfig holds media storage and delivery configuration
|
||||
@@ -62,6 +64,18 @@ type MediaConfig struct {
|
||||
BaseURL string `json:"base_url,omitempty"` // Base URL for media links
|
||||
}
|
||||
|
||||
// EventLoggerConfig holds event logging configuration
|
||||
type EventLoggerConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Targets []string `json:"targets"` // "file", "sqlite", "postgres"
|
||||
|
||||
// File-based logging
|
||||
FileDir string `json:"file_dir,omitempty"` // Base directory for event files
|
||||
|
||||
// Database logging (uses main Database config for connection)
|
||||
TableName string `json:"table_name,omitempty"` // Table name for event logs (default: "event_logs")
|
||||
}
|
||||
|
||||
// Load reads configuration from a file
|
||||
func Load(path string) (*Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
@@ -90,6 +104,15 @@ func Load(path string) (*Config, error) {
|
||||
if cfg.Media.Mode == "" {
|
||||
cfg.Media.Mode = "link"
|
||||
}
|
||||
if cfg.EventLogger.FileDir == "" {
|
||||
cfg.EventLogger.FileDir = "./data/events"
|
||||
}
|
||||
if cfg.EventLogger.TableName == "" {
|
||||
cfg.EventLogger.TableName = "event_logs"
|
||||
}
|
||||
if cfg.Database.SQLitePath == "" {
|
||||
cfg.Database.SQLitePath = "./data/events.db"
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
102
internal/eventlogger/eventlogger.go
Normal file
102
internal/eventlogger/eventlogger.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package eventlogger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"git.warky.dev/wdevs/whatshooked/internal/config"
|
||||
"git.warky.dev/wdevs/whatshooked/internal/events"
|
||||
"git.warky.dev/wdevs/whatshooked/internal/logging"
|
||||
)
|
||||
|
||||
// Logger handles event logging to multiple targets
|
||||
type Logger struct {
|
||||
config config.EventLoggerConfig
|
||||
dbConfig config.DatabaseConfig
|
||||
targets []Target
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// Target represents a logging target
|
||||
type Target interface {
|
||||
Log(event events.Event) error
|
||||
Close() error
|
||||
}
|
||||
|
||||
// NewLogger creates a new event logger
|
||||
func NewLogger(cfg config.EventLoggerConfig, dbConfig config.DatabaseConfig) (*Logger, error) {
|
||||
logger := &Logger{
|
||||
config: cfg,
|
||||
dbConfig: dbConfig,
|
||||
targets: make([]Target, 0),
|
||||
}
|
||||
|
||||
// Initialize targets based on configuration
|
||||
for _, targetType := range cfg.Targets {
|
||||
switch strings.ToLower(targetType) {
|
||||
case "file":
|
||||
fileTarget, err := NewFileTarget(cfg.FileDir)
|
||||
if err != nil {
|
||||
logging.Error("Failed to initialize file target", "error", err)
|
||||
continue
|
||||
}
|
||||
logger.targets = append(logger.targets, fileTarget)
|
||||
logging.Info("Event logger file target initialized", "dir", cfg.FileDir)
|
||||
|
||||
case "sqlite":
|
||||
sqliteTarget, err := NewSQLiteTarget(dbConfig, cfg.TableName)
|
||||
if err != nil {
|
||||
logging.Error("Failed to initialize SQLite target", "error", err)
|
||||
continue
|
||||
}
|
||||
logger.targets = append(logger.targets, sqliteTarget)
|
||||
logging.Info("Event logger SQLite target initialized")
|
||||
|
||||
case "postgres", "postgresql":
|
||||
postgresTarget, err := NewPostgresTarget(dbConfig, cfg.TableName)
|
||||
if err != nil {
|
||||
logging.Error("Failed to initialize PostgreSQL target", "error", err)
|
||||
continue
|
||||
}
|
||||
logger.targets = append(logger.targets, postgresTarget)
|
||||
logging.Info("Event logger PostgreSQL target initialized")
|
||||
|
||||
default:
|
||||
logging.Error("Unknown event logger target type", "type", targetType)
|
||||
}
|
||||
}
|
||||
|
||||
return logger, nil
|
||||
}
|
||||
|
||||
// Log logs an event to all configured targets
|
||||
func (l *Logger) Log(event events.Event) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
for _, target := range l.targets {
|
||||
if err := target.Log(event); err != nil {
|
||||
logging.Error("Failed to log event", "target", fmt.Sprintf("%T", target), "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes all logging targets
|
||||
func (l *Logger) Close() error {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
var errors []string
|
||||
for _, target := range l.targets {
|
||||
if err := target.Close(); err != nil {
|
||||
errors = append(errors, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
return fmt.Errorf("errors closing targets: %s", strings.Join(errors, "; "))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
69
internal/eventlogger/file_target.go
Normal file
69
internal/eventlogger/file_target.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package eventlogger
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"git.warky.dev/wdevs/whatshooked/internal/events"
|
||||
)
|
||||
|
||||
// FileTarget logs events to organized file structure
|
||||
type FileTarget struct {
|
||||
baseDir string
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewFileTarget creates a new file-based logging target
|
||||
func NewFileTarget(baseDir string) (*FileTarget, error) {
|
||||
// Create base directory if it doesn't exist
|
||||
if err := os.MkdirAll(baseDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create event log directory: %w", err)
|
||||
}
|
||||
|
||||
return &FileTarget{
|
||||
baseDir: baseDir,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Log writes an event to a file
|
||||
func (ft *FileTarget) Log(event events.Event) error {
|
||||
ft.mu.Lock()
|
||||
defer ft.mu.Unlock()
|
||||
|
||||
// Create directory structure: baseDir/[type]/[YYYYMMDD]/
|
||||
eventType := string(event.Type)
|
||||
dateDir := event.Timestamp.Format("20060102")
|
||||
dirPath := filepath.Join(ft.baseDir, eventType, dateDir)
|
||||
|
||||
if err := os.MkdirAll(dirPath, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create event directory: %w", err)
|
||||
}
|
||||
|
||||
// Create filename: [hh24_mi_ss]_[type].json
|
||||
filename := fmt.Sprintf("%s_%s.json",
|
||||
event.Timestamp.Format("15_04_05"),
|
||||
eventType,
|
||||
)
|
||||
filePath := filepath.Join(dirPath, filename)
|
||||
|
||||
// Marshal event to JSON
|
||||
data, err := json.MarshalIndent(event, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal event: %w", err)
|
||||
}
|
||||
|
||||
// Write to file
|
||||
if err := os.WriteFile(filePath, data, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write event file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes the file target (no-op for file target)
|
||||
func (ft *FileTarget) Close() error {
|
||||
return nil
|
||||
}
|
||||
120
internal/eventlogger/postgres_target.go
Normal file
120
internal/eventlogger/postgres_target.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package eventlogger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.warky.dev/wdevs/whatshooked/internal/config"
|
||||
"git.warky.dev/wdevs/whatshooked/internal/events"
|
||||
|
||||
_ "github.com/lib/pq" // PostgreSQL driver
|
||||
)
|
||||
|
||||
// PostgresTarget logs events to PostgreSQL database
|
||||
type PostgresTarget struct {
|
||||
db *sql.DB
|
||||
tableName string
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewPostgresTarget creates a new PostgreSQL logging target
|
||||
func NewPostgresTarget(dbConfig config.DatabaseConfig, tableName string) (*PostgresTarget, error) {
|
||||
// Build connection string
|
||||
connStr := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
|
||||
dbConfig.Host,
|
||||
dbConfig.Port,
|
||||
dbConfig.Username,
|
||||
dbConfig.Password,
|
||||
dbConfig.Database,
|
||||
)
|
||||
|
||||
// Open PostgreSQL connection
|
||||
db, err := sql.Open("postgres", connStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open PostgreSQL database: %w", err)
|
||||
}
|
||||
|
||||
// Test connection
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := db.PingContext(ctx); err != nil {
|
||||
db.Close()
|
||||
return nil, fmt.Errorf("failed to connect to PostgreSQL: %w", err)
|
||||
}
|
||||
|
||||
target := &PostgresTarget{
|
||||
db: db,
|
||||
tableName: tableName,
|
||||
}
|
||||
|
||||
// Create table if it doesn't exist
|
||||
if err := target.createTable(); err != nil {
|
||||
db.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return target, nil
|
||||
}
|
||||
|
||||
// createTable creates the event logs table if it doesn't exist
|
||||
func (pt *PostgresTarget) createTable() error {
|
||||
query := fmt.Sprintf(`
|
||||
CREATE TABLE IF NOT EXISTS %s (
|
||||
id SERIAL PRIMARY KEY,
|
||||
event_type VARCHAR(100) NOT NULL,
|
||||
timestamp TIMESTAMP NOT NULL,
|
||||
data JSONB NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`, pt.tableName)
|
||||
|
||||
if _, err := pt.db.Exec(query); err != nil {
|
||||
return fmt.Errorf("failed to create table: %w", err)
|
||||
}
|
||||
|
||||
// Create index on event_type and timestamp
|
||||
indexQuery := fmt.Sprintf(`
|
||||
CREATE INDEX IF NOT EXISTS idx_%s_type_timestamp
|
||||
ON %s(event_type, timestamp)
|
||||
`, pt.tableName, pt.tableName)
|
||||
|
||||
if _, err := pt.db.Exec(indexQuery); err != nil {
|
||||
return fmt.Errorf("failed to create index: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Log writes an event to PostgreSQL database
|
||||
func (pt *PostgresTarget) Log(event events.Event) error {
|
||||
pt.mu.Lock()
|
||||
defer pt.mu.Unlock()
|
||||
|
||||
// Marshal event data to JSON
|
||||
data, err := json.Marshal(event.Data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal event data: %w", err)
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(`
|
||||
INSERT INTO %s (event_type, timestamp, data)
|
||||
VALUES ($1, $2, $3)
|
||||
`, pt.tableName)
|
||||
|
||||
_, err = pt.db.Exec(query, string(event.Type), event.Timestamp, string(data))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to insert event: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes the PostgreSQL database connection
|
||||
func (pt *PostgresTarget) Close() error {
|
||||
return pt.db.Close()
|
||||
}
|
||||
111
internal/eventlogger/sqlite_target.go
Normal file
111
internal/eventlogger/sqlite_target.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package eventlogger
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"git.warky.dev/wdevs/whatshooked/internal/config"
|
||||
"git.warky.dev/wdevs/whatshooked/internal/events"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3" // SQLite driver
|
||||
)
|
||||
|
||||
// SQLiteTarget logs events to SQLite database
|
||||
type SQLiteTarget struct {
|
||||
db *sql.DB
|
||||
tableName string
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewSQLiteTarget creates a new SQLite logging target
|
||||
func NewSQLiteTarget(dbConfig config.DatabaseConfig, tableName string) (*SQLiteTarget, error) {
|
||||
// Use the SQLite path from config (defaults to "./data/events.db")
|
||||
dbPath := dbConfig.SQLitePath
|
||||
|
||||
// Create directory if needed
|
||||
dir := filepath.Dir(dbPath)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create database directory: %w", err)
|
||||
}
|
||||
|
||||
// Open SQLite connection
|
||||
db, err := sql.Open("sqlite3", dbPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open SQLite database: %w", err)
|
||||
}
|
||||
|
||||
target := &SQLiteTarget{
|
||||
db: db,
|
||||
tableName: tableName,
|
||||
}
|
||||
|
||||
// Create table if it doesn't exist
|
||||
if err := target.createTable(); err != nil {
|
||||
db.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return target, nil
|
||||
}
|
||||
|
||||
// createTable creates the event logs table if it doesn't exist
|
||||
func (st *SQLiteTarget) createTable() error {
|
||||
query := fmt.Sprintf(`
|
||||
CREATE TABLE IF NOT EXISTS %s (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
event_type TEXT NOT NULL,
|
||||
timestamp DATETIME NOT NULL,
|
||||
data TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`, st.tableName)
|
||||
|
||||
if _, err := st.db.Exec(query); err != nil {
|
||||
return fmt.Errorf("failed to create table: %w", err)
|
||||
}
|
||||
|
||||
// Create index on event_type and timestamp
|
||||
indexQuery := fmt.Sprintf(`
|
||||
CREATE INDEX IF NOT EXISTS idx_%s_type_timestamp
|
||||
ON %s(event_type, timestamp)
|
||||
`, st.tableName, st.tableName)
|
||||
|
||||
if _, err := st.db.Exec(indexQuery); err != nil {
|
||||
return fmt.Errorf("failed to create index: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Log writes an event to SQLite database
|
||||
func (st *SQLiteTarget) Log(event events.Event) error {
|
||||
st.mu.Lock()
|
||||
defer st.mu.Unlock()
|
||||
|
||||
// Marshal event data to JSON
|
||||
data, err := json.Marshal(event.Data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal event data: %w", err)
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(`
|
||||
INSERT INTO %s (event_type, timestamp, data)
|
||||
VALUES (?, ?, ?)
|
||||
`, st.tableName)
|
||||
|
||||
_, err = st.db.Exec(query, string(event.Type), event.Timestamp, string(data))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to insert event: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes the SQLite database connection
|
||||
func (st *SQLiteTarget) Close() error {
|
||||
return st.db.Close()
|
||||
}
|
||||
Reference in New Issue
Block a user