248 lines
5.4 KiB
Go
248 lines
5.4 KiB
Go
// FILE: logwisp/src/internal/sink/console.go
|
|
package sink
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"logwisp/src/internal/format"
|
|
"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 int64
|
|
}
|
|
|
|
// 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
|
|
formatter format.Formatter
|
|
|
|
// Statistics
|
|
totalProcessed atomic.Uint64
|
|
lastProcessed atomic.Value // time.Time
|
|
}
|
|
|
|
// NewStdoutSink creates a new stdout sink
|
|
func NewStdoutSink(options map[string]any, logger *log.Logger, formatter format.Formatter) (*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 := options["buffer_size"].(int64); 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,
|
|
formatter: formatter,
|
|
}
|
|
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
|
|
formatted, err := s.formatter.Format(entry)
|
|
if err != nil {
|
|
s.logger.Error("msg", "Failed to format log entry for stdout", "error", err)
|
|
continue
|
|
}
|
|
s.output.Write(formatted)
|
|
|
|
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
|
|
formatter format.Formatter
|
|
|
|
// Statistics
|
|
totalProcessed atomic.Uint64
|
|
lastProcessed atomic.Value // time.Time
|
|
}
|
|
|
|
// NewStderrSink creates a new stderr sink
|
|
func NewStderrSink(options map[string]any, logger *log.Logger, formatter format.Formatter) (*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 := options["buffer_size"].(int64); 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,
|
|
formatter: formatter,
|
|
}
|
|
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
|
|
formatted, err := s.formatter.Format(entry)
|
|
if err != nil {
|
|
s.logger.Error("msg", "Failed to format log entry for stderr", "error", err)
|
|
continue
|
|
}
|
|
s.output.Write(formatted)
|
|
|
|
case <-ctx.Done():
|
|
return
|
|
case <-s.done:
|
|
return
|
|
}
|
|
}
|
|
} |