// FILE: logwisp/src/internal/sink/console.go package console import ( "context" "fmt" "io" "os" "sync/atomic" "time" "logwisp/src/internal/config" "logwisp/src/internal/core" "logwisp/src/internal/plugin" "logwisp/src/internal/session" "logwisp/src/internal/sink" lconfig "github.com/lixenwraith/config" "github.com/lixenwraith/log" ) // init registers the component in plugin factory func init() { if err := plugin.RegisterSink("console", NewConsoleSinkPlugin); err != nil { panic(fmt.Sprintf("failed to register console sink: %v", err)) } } // ConsoleSink writes log entries to the console (stdout/stderr) using an dedicated logger instance type ConsoleSink struct { // Plugin identity and session management id string proxy *session.Proxy session *session.Session // Configuration config *config.ConsoleSinkOptions // Application input chan core.TransportEvent output io.Writer logger *log.Logger // application logger // Runtime done chan struct{} startTime time.Time // Statistics totalProcessed atomic.Uint64 lastProcessed atomic.Value // time.Time } // NewConsoleSinkPlugin creates a console sink through plugin factory func NewConsoleSinkPlugin( id string, configMap map[string]any, logger *log.Logger, proxy *session.Proxy, ) (sink.Sink, error) { // Step 1: Create empty config struct with defaults opts := &config.ConsoleSinkOptions{ Target: "stdout", // Default target BufferSize: 1000, // Default buffer size } // Step 2: Use lconfig to scan map into struct (overriding defaults) cfg := lconfig.New() for path, value := range lconfig.FlattenMap(configMap, "") { cfg.Set(path, value) } if err := cfg.Scan(opts); err != nil { return nil, fmt.Errorf("failed to parse config: %w", err) } // Step 3: Validate required fields // Target validation var output io.Writer switch opts.Target { case "stdout": output = os.Stdout case "stderr": output = os.Stderr default: return nil, fmt.Errorf("invalid console target: %s (must be 'stdout' or 'stderr')", opts.Target) } if opts.BufferSize <= 0 { opts.BufferSize = 1000 } // Step 4: Create and return plugin instance cs := &ConsoleSink{ id: id, proxy: proxy, config: opts, input: make(chan core.TransportEvent, opts.BufferSize), output: output, done: make(chan struct{}), startTime: time.Now(), logger: logger, } cs.lastProcessed.Store(time.Time{}) // Create session for output cs.session = proxy.CreateSession( fmt.Sprintf("console:%s", opts.Target), map[string]any{ "instance_id": id, "type": "console", "target": opts.Target, }, ) cs.logger.Info("msg", "Console sink initialized", "component", "console_sink", "instance_id", id, "target", opts.Target, ) return cs, nil } // Capabilities returns supported capabilities func (cs *ConsoleSink) Capabilities() []core.Capability { return []core.Capability{ core.CapSessionAware, // Single output session } } // Input returns the channel for sending transport events func (cs *ConsoleSink) Input() chan<- core.TransportEvent { return cs.input } // Start begins the processing loop func (cs *ConsoleSink) Start(ctx context.Context) error { go cs.processLoop(ctx) cs.logger.Info("msg", "Console sink started", "component", "console_sink", "target", cs.config.Target) return nil } // Stop gracefully shuts down the sink func (cs *ConsoleSink) Stop() { cs.logger.Info("msg", "Stopping console sink", "target", cs.config.Target) // Remove session if cs.session != nil { cs.proxy.RemoveSession(cs.session.ID) } close(cs.done) cs.logger.Info("msg", "Console sink stopped", "instance_id", cs.id, "target", cs.config.Target, "instance_id", cs.id, ) } // GetStats returns sink statistics func (cs *ConsoleSink) GetStats() sink.SinkStats { lastProc, _ := cs.lastProcessed.Load().(time.Time) return sink.SinkStats{ ID: cs.id, Type: "console", TotalProcessed: cs.totalProcessed.Load(), StartTime: cs.startTime, LastProcessed: lastProc, Details: map[string]any{ "target": cs.config.Target, "buffer_size": cs.config.BufferSize, }, } } // processLoop reads transport events and writes to console func (cs *ConsoleSink) processLoop(ctx context.Context) { for { select { case event, ok := <-cs.input: if !ok { return } // Write pre-formatted payload directly to output if _, err := cs.output.Write(event.Payload); err != nil { cs.logger.Error("msg", "Failed to write to console", "component", "console_sink", "target", cs.config.Target, "error", err) continue } cs.totalProcessed.Add(1) cs.lastProcessed.Store(time.Now()) case <-ctx.Done(): return case <-cs.done: return } } }