128 lines
3.1 KiB
Go
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
|
|
}
|