// FILE: logwisp/src/internal/sink/console.go package sink import ( "context" "fmt" "strings" "sync/atomic" "time" "logwisp/src/internal/config" "logwisp/src/internal/core" "logwisp/src/internal/format" "github.com/lixenwraith/log" ) // ConsoleSink writes log entries to the console (stdout/stderr) using an dedicated logger instance. type ConsoleSink struct { // Configuration config *config.ConsoleSinkOptions // Application input chan core.LogEntry writer *log.Logger // dedicated logger for console output formatter format.Formatter logger *log.Logger // application logger // Runtime done chan struct{} startTime time.Time // Statistics totalProcessed atomic.Uint64 lastProcessed atomic.Value // time.Time } // NewConsoleSink creates a new console sink. func NewConsoleSink(opts *config.ConsoleSinkOptions, appLogger *log.Logger, formatter format.Formatter) (*ConsoleSink, error) { if opts == nil { return nil, fmt.Errorf("console sink options cannot be nil") } // Set defaults if not configured if opts.Target == "" { opts.Target = "stdout" } if opts.BufferSize <= 0 { opts.BufferSize = 1000 } // Dedicated logger instance as console writer writer, err := log.NewBuilder(). EnableFile(false). EnableConsole(true). ConsoleTarget(opts.Target). Format("raw"). // Passthrough pre-formatted messages ShowTimestamp(false). // Disable writer's own timestamp ShowLevel(false). // Disable writer's own level prefix Build() if err != nil { return nil, fmt.Errorf("failed to create console writer: %w", err) } s := &ConsoleSink{ config: opts, input: make(chan core.LogEntry, opts.BufferSize), writer: writer, done: make(chan struct{}), startTime: time.Now(), logger: appLogger, formatter: formatter, } s.lastProcessed.Store(time.Time{}) return s, nil } // Input returns the channel for sending log entries. func (s *ConsoleSink) Input() chan<- core.LogEntry { return s.input } // Start begins the processing loop for the sink. func (s *ConsoleSink) Start(ctx context.Context) error { // Start the internal writer's processing goroutine. if err := s.writer.Start(); err != nil { return fmt.Errorf("failed to start console writer: %w", err) } go s.processLoop(ctx) s.logger.Info("msg", "Console sink started", "component", "console_sink", "target", s.writer.GetConfig().ConsoleTarget) return nil } // Stop gracefully shuts down the sink. func (s *ConsoleSink) Stop() { target := s.writer.GetConfig().ConsoleTarget s.logger.Info("msg", "Stopping console sink", "target", target) close(s.done) // Shutdown the internal writer with a timeout. if err := s.writer.Shutdown(2 * time.Second); err != nil { s.logger.Error("msg", "Error shutting down console writer", "component", "console_sink", "error", err) } s.logger.Info("msg", "Console sink stopped", "target", target) } // GetStats returns the sink's statistics. func (s *ConsoleSink) GetStats() SinkStats { lastProc, _ := s.lastProcessed.Load().(time.Time) return SinkStats{ Type: "console", TotalProcessed: s.totalProcessed.Load(), StartTime: s.startTime, LastProcessed: lastProc, Details: map[string]any{ "target": s.writer.GetConfig().ConsoleTarget, }, } } // processLoop reads entries, formats them, and writes to the console. func (s *ConsoleSink) processLoop(ctx context.Context) { for { select { case entry, ok := <-s.input: if !ok { return } s.totalProcessed.Add(1) s.lastProcessed.Store(time.Now()) // Format the entry using the pipeline's configured formatter. formatted, err := s.formatter.Format(entry) if err != nil { s.logger.Error("msg", "Failed to format log entry for console", "component", "console_sink", "error", err) continue } // Convert to string to prevent hex encoding of []byte by log package message := string(formatted) switch strings.ToUpper(entry.Level) { case "DEBUG": s.writer.Debug(message) case "INFO": s.writer.Info(message) case "WARN", "WARNING": s.writer.Warn(message) case "ERROR", "FATAL": s.writer.Error(message) default: s.writer.Message(message) } case <-ctx.Done(): return case <-s.done: return } } }