// FILE: src/internal/sink/console.go package sink import ( "context" "fmt" "io" "os" "strings" "sync/atomic" "time" "logwisp/src/internal/source" "github.com/lixenwraith/log" ) // ConsoleConfig holds common configuration for console sinks type ConsoleConfig struct { Target string // "stdout", "stderr", or "split" BufferSize int } // StdoutSink writes log entries to stdout type StdoutSink struct { input chan source.LogEntry config ConsoleConfig output io.Writer done chan struct{} startTime time.Time logger *log.Logger // Statistics totalProcessed atomic.Uint64 lastProcessed atomic.Value // time.Time } // NewStdoutSink creates a new stdout sink func NewStdoutSink(options map[string]any, logger *log.Logger) (*StdoutSink, error) { config := ConsoleConfig{ Target: "stdout", BufferSize: 1000, } // Check for split mode configuration if target, ok := options["target"].(string); ok { config.Target = target } if bufSize, ok := toInt(options["buffer_size"]); ok && bufSize > 0 { config.BufferSize = bufSize } s := &StdoutSink{ input: make(chan source.LogEntry, config.BufferSize), config: config, output: os.Stdout, done: make(chan struct{}), startTime: time.Now(), logger: logger, } s.lastProcessed.Store(time.Time{}) return s, nil } func (s *StdoutSink) Input() chan<- source.LogEntry { return s.input } func (s *StdoutSink) Start(ctx context.Context) error { go s.processLoop(ctx) s.logger.Info("msg", "Stdout sink started", "component", "stdout_sink", "target", s.config.Target) return nil } func (s *StdoutSink) Stop() { s.logger.Info("msg", "Stopping stdout sink") close(s.done) s.logger.Info("msg", "Stdout sink stopped") } func (s *StdoutSink) GetStats() SinkStats { lastProc, _ := s.lastProcessed.Load().(time.Time) return SinkStats{ Type: "stdout", TotalProcessed: s.totalProcessed.Load(), StartTime: s.startTime, LastProcessed: lastProc, Details: map[string]any{ "target": s.config.Target, }, } } func (s *StdoutSink) processLoop(ctx context.Context) { for { select { case entry, ok := <-s.input: if !ok { return } s.totalProcessed.Add(1) s.lastProcessed.Store(time.Now()) // Handle split mode - only process INFO/DEBUG for stdout if s.config.Target == "split" { upperLevel := strings.ToUpper(entry.Level) if upperLevel == "ERROR" || upperLevel == "WARN" || upperLevel == "WARNING" { // Skip ERROR/WARN levels in stdout when in split mode continue } } // Format and write timestamp := entry.Time.Format(time.RFC3339Nano) level := entry.Level if level == "" { level = "INFO" } // Direct write to stdout fmt.Fprintf(s.output, "[%s] %s %s\n", timestamp, level, entry.Message) case <-ctx.Done(): return case <-s.done: return } } } // StderrSink writes log entries to stderr type StderrSink struct { input chan source.LogEntry config ConsoleConfig output io.Writer done chan struct{} startTime time.Time logger *log.Logger // Statistics totalProcessed atomic.Uint64 lastProcessed atomic.Value // time.Time } // NewStderrSink creates a new stderr sink func NewStderrSink(options map[string]any, logger *log.Logger) (*StderrSink, error) { config := ConsoleConfig{ Target: "stderr", BufferSize: 1000, } // Check for split mode configuration if target, ok := options["target"].(string); ok { config.Target = target } if bufSize, ok := toInt(options["buffer_size"]); ok && bufSize > 0 { config.BufferSize = bufSize } s := &StderrSink{ input: make(chan source.LogEntry, config.BufferSize), config: config, output: os.Stderr, done: make(chan struct{}), startTime: time.Now(), logger: logger, } s.lastProcessed.Store(time.Time{}) return s, nil } func (s *StderrSink) Input() chan<- source.LogEntry { return s.input } func (s *StderrSink) Start(ctx context.Context) error { go s.processLoop(ctx) s.logger.Info("msg", "Stderr sink started", "component", "stderr_sink", "target", s.config.Target) return nil } func (s *StderrSink) Stop() { s.logger.Info("msg", "Stopping stderr sink") close(s.done) s.logger.Info("msg", "Stderr sink stopped") } func (s *StderrSink) GetStats() SinkStats { lastProc, _ := s.lastProcessed.Load().(time.Time) return SinkStats{ Type: "stderr", TotalProcessed: s.totalProcessed.Load(), StartTime: s.startTime, LastProcessed: lastProc, Details: map[string]any{ "target": s.config.Target, }, } } func (s *StderrSink) processLoop(ctx context.Context) { for { select { case entry, ok := <-s.input: if !ok { return } s.totalProcessed.Add(1) s.lastProcessed.Store(time.Now()) // Handle split mode - only process ERROR/WARN for stderr if s.config.Target == "split" { upperLevel := strings.ToUpper(entry.Level) if upperLevel != "ERROR" && upperLevel != "WARN" && upperLevel != "WARNING" { // Skip non-ERROR/WARN levels in stderr when in split mode continue } } // Format and write timestamp := entry.Time.Format(time.RFC3339Nano) level := entry.Level if level == "" { level = "INFO" } // Direct write to stderr fmt.Fprintf(s.output, "[%s] %s %s\n", timestamp, level, entry.Message) case <-ctx.Done(): return case <-s.done: return } } }