Files
Charon/backend/internal/logger/logger.go
2026-01-26 19:22:05 +00:00

128 lines
3.1 KiB
Go

// Package logger provides logging functionality with broadcast capabilities for real-time log streaming.
package logger
import (
"io"
"os"
"sync"
"github.com/sirupsen/logrus"
)
var _log = logrus.New()
var _broadcastHook *BroadcastHook
// Init initializes the global logger with output writer and debug level.
func Init(debug bool, out io.Writer) {
if out == nil {
out = os.Stdout
}
_log.SetOutput(out)
if debug {
_log.SetLevel(logrus.DebugLevel)
_log.SetFormatter(&logrus.TextFormatter{FullTimestamp: true})
} else {
_log.SetLevel(logrus.InfoLevel)
_log.SetFormatter(&logrus.JSONFormatter{})
}
// Initialize and add broadcast hook
_broadcastHook = NewBroadcastHook()
_log.AddHook(_broadcastHook)
}
// Log returns a standard logger entry to use across packages.
func Log() *logrus.Entry {
return logrus.NewEntry(_log)
}
// WithFields returns a logger entry with provided fields.
func WithFields(fields logrus.Fields) *logrus.Entry {
return Log().WithFields(fields)
}
// GetBroadcastHook returns the global broadcast hook instance.
func GetBroadcastHook() *BroadcastHook {
if _broadcastHook == nil {
_broadcastHook = NewBroadcastHook()
_log.AddHook(_broadcastHook)
}
return _broadcastHook
}
// BroadcastHook implements logrus.Hook to broadcast log entries to active listeners.
type BroadcastHook struct {
mu sync.RWMutex
listeners map[string]chan *logrus.Entry
}
// NewBroadcastHook creates a new BroadcastHook instance.
func NewBroadcastHook() *BroadcastHook {
return &BroadcastHook{
listeners: make(map[string]chan *logrus.Entry),
}
}
// Levels returns all log levels that this hook should fire for.
func (h *BroadcastHook) Levels() []logrus.Level {
return logrus.AllLevels
}
// Fire broadcasts the log entry to all active listeners.
func (h *BroadcastHook) Fire(entry *logrus.Entry) error {
h.mu.RLock()
defer h.mu.RUnlock()
// Broadcast to all listeners (non-blocking)
for _, ch := range h.listeners {
select {
case ch <- entry:
default:
// Skip if channel is full (prevents blocking)
}
}
return nil
}
// Subscribe adds a new listener and returns a channel for receiving log entries.
// The caller must call Unsubscribe when done to prevent resource leaks.
func (h *BroadcastHook) Subscribe(id string) <-chan *logrus.Entry {
h.mu.Lock()
defer h.mu.Unlock()
ch := make(chan *logrus.Entry, 100) // Buffer to prevent blocking
h.listeners[id] = ch
return ch
}
// Unsubscribe removes a listener and closes its channel.
func (h *BroadcastHook) Unsubscribe(id string) {
h.mu.Lock()
defer h.mu.Unlock()
if ch, ok := h.listeners[id]; ok {
close(ch)
delete(h.listeners, id)
}
}
// ActiveListeners returns the count of active listeners.
func (h *BroadcastHook) ActiveListeners() int {
h.mu.RLock()
defer h.mu.RUnlock()
return len(h.listeners)
}
// ListenerIDs returns the IDs of all active listeners. Intended for tests/observability only.
func (h *BroadcastHook) ListenerIDs() []string {
h.mu.RLock()
defer h.mu.RUnlock()
ids := make([]string, 0, len(h.listeners))
for id := range h.listeners {
ids = append(ids, id)
}
return ids
}