internal/logger/logger.go

package logger

import (
	"fmt"
	"honeypot/internal/database"
	"honeypot/internal/types"
	"io"
	"log/slog"
	"net/http"
	"os"
	"path/filepath"
	"time"

	"github.com/prometheus/client_golang/prometheus"
)

// MetricsCollector is an interface for recording metrics from events.
type MetricsCollector interface {
	RecordEvent(e types.LogEvent)
	GetHandler() http.Handler
	SetDatabase(db *database.Database)
}

// EventSink is an optional sink for streaming honeypot events to external
// consumers (for example, the internal websocket dashboard).
type EventSink interface {
	EmitEvent(e types.LogEvent)
}

// TopNFieldRegistrar is an interface for registering fields for top-N tracking.
type TopNFieldRegistrar interface {
	RegisterTopNField(honeypotType, fieldName string)
}

// RegisterTopNField registers a field for top-N tracking for a specific honeypot type.
func RegisterTopNField(honeypotType, fieldName string) {
	if globalMetricsCollector != nil {
		if registrar, ok := globalMetricsCollector.(TopNFieldRegistrar); ok {
			registrar.RegisterTopNField(honeypotType, fieldName)
		}
	}
}

// RegisterCollector registers a Prometheus collector for metrics export.
// This allows honeypots to register their own custom metrics.
func RegisterCollector(c prometheus.Collector) error {
	return prometheus.Register(c)
}

var globalMetricsCollector MetricsCollector
var globalEventSink EventSink
var globalDatabase database.Database

// Setup configures and returns a JSON logger with optional metrics collector.
// maxLogSize is the maximum log file size in bytes before rotation (0 = no rotation, uses regular file).
func Setup(logFile string) (*slog.Logger, error) {
	var output io.Writer = os.Stdout
	if logFile != "" {
		// Create the log file directory if it doesn't exist
		dir := filepath.Dir(logFile)
		if _, err := os.Stat(dir); os.IsNotExist(err) {
			if err := os.MkdirAll(dir, 0700); err != nil {
				return nil, err
			}
		}
		// Create the log file if it doesn't exist, and open for appending
		f, err := os.OpenFile(logFile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
		if err != nil {
			return nil, err
		}
		output = f
	}

	handler := slog.NewJSONHandler(output, &slog.HandlerOptions{
		Level: slog.LevelInfo,
	})
	logger := slog.New(handler)
	slog.SetDefault(logger)
	return logger, nil
}

func RegisterMetricsCollector(metrics MetricsCollector) {
	globalMetricsCollector = metrics
}

func RegisterDatabase(database *database.Database) {
	globalDatabase = *database
}

// RegisterEventSink sets the global sink used for streaming honeypot events.
// It is safe to call this during startup before events are logged.
func RegisterEventSink(sink EventSink) {
	globalEventSink = sink
}

// LogEvent logs a standardized honeypot event in JSON format.
func LogEvent(logger *slog.Logger, e types.LogEvent) {
	attrs := []any{
		"type", e.Type,
		"event", e.Event,
	}

	if e.RemoteAddr != "" {
		attrs = append(attrs, "remote_addr", e.RemoteAddr)
	}

	if e.RemotePort != 0 {
		attrs = append(attrs, "remote_port", e.RemotePort)
	}

	if e.DstPort != 0 {
		attrs = append(attrs, "dst_port", e.DstPort)
	}

	// Add all additional fields
	for k, v := range e.Fields {
		attrs = append(attrs, k, v)
	}

	logger.Info("honeypot_event", attrs...)

	// Record event in metrics if collector is available
	if globalMetricsCollector != nil {
		globalMetricsCollector.RecordEvent(e)
	}

	e.Time = time.Now().UTC().Format(time.RFC3339Nano)

	// Stream event to external sink if configured.
	if globalEventSink != nil {
		globalEventSink.EmitEvent(e)
	}

	// Record event in database if database is available
	if globalDatabase.DB != nil {
		err := globalDatabase.InsertEvent(&e)
		if err != nil {
			logger.Error("failed to insert event into database", "error", err)
		}
	}
}

// LogError logs an error event.
func LogError(logger *slog.Logger, honeypotType types.HoneypotType, event string, err error, args []any) {
	attrs := []any{
		"event", event,
		"error", err,
	}

	attrs = append(attrs, args...)

	logger.Error("honeypot_error", attrs...)
	fmt.Fprintf(os.Stderr, "%s: %s: %v %v\n", "honeypot_error", event, err, args)
}

// LogInfo logs an informational message with honeypot context.
func LogInfo(logger *slog.Logger, honeypotType types.HoneypotType, event string, args []any) {
	attrs := []any{
		"type", honeypotType,
		"event", event,
	}

	attrs = append(attrs, args...)

	logger.Info("honeypot_info", attrs...)
	fmt.Fprintf(os.Stdout, "%s: %s: %v\n", "honeypot_info", event, args)
}