173 lines
4.4 KiB
Go
173 lines
4.4 KiB
Go
// FILE: logwisp/src/internal/sink/file.go
|
|
package sink
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"logwisp/src/internal/config"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"logwisp/src/internal/core"
|
|
"logwisp/src/internal/format"
|
|
|
|
"github.com/lixenwraith/log"
|
|
)
|
|
|
|
// Writes log entries to files with rotation
|
|
type FileSink struct {
|
|
input chan core.LogEntry
|
|
writer *log.Logger // Internal logger instance for file writing
|
|
done chan struct{}
|
|
startTime time.Time
|
|
logger *log.Logger // Application logger
|
|
formatter format.Formatter
|
|
|
|
// Statistics
|
|
totalProcessed atomic.Uint64
|
|
lastProcessed atomic.Value // time.Time
|
|
}
|
|
|
|
// Creates a new file sink
|
|
func NewFileSink(options map[string]any, logger *log.Logger, formatter format.Formatter) (*FileSink, error) {
|
|
directory, ok := options["directory"].(string)
|
|
if !ok || directory == "" {
|
|
directory = "./"
|
|
logger.Warn("No directory or invalid directory provided, current directory will be used")
|
|
}
|
|
|
|
name, ok := options["name"].(string)
|
|
if !ok || name == "" {
|
|
name = "logwisp.output"
|
|
logger.Warn(fmt.Sprintf("No filename provided, %s will be used", name))
|
|
}
|
|
|
|
// Create configuration for the internal log writer
|
|
writerConfig := log.DefaultConfig()
|
|
writerConfig.Directory = directory
|
|
writerConfig.Name = name
|
|
writerConfig.EnableConsole = false // File only
|
|
writerConfig.ShowTimestamp = false // We already have timestamps in entries
|
|
writerConfig.ShowLevel = false // We already have levels in entries
|
|
|
|
// Add optional configurations
|
|
if maxSize, ok := options["max_size_mb"].(int64); ok && maxSize > 0 {
|
|
writerConfig.MaxSizeKB = maxSize * 1000
|
|
}
|
|
|
|
if maxTotalSize, ok := options["max_total_size_mb"].(int64); ok && maxTotalSize >= 0 {
|
|
writerConfig.MaxTotalSizeKB = maxTotalSize * 1000
|
|
}
|
|
|
|
if retention, ok := options["retention_hours"].(int64); ok && retention > 0 {
|
|
writerConfig.RetentionPeriodHrs = float64(retention)
|
|
}
|
|
|
|
if minDiskFree, ok := options["min_disk_free_mb"].(int64); ok && minDiskFree > 0 {
|
|
writerConfig.MinDiskFreeKB = minDiskFree * 1000
|
|
}
|
|
|
|
// Create internal logger for file writing
|
|
writer := log.NewLogger()
|
|
if err := writer.ApplyConfig(writerConfig); err != nil {
|
|
return nil, fmt.Errorf("failed to initialize file writer: %w", err)
|
|
}
|
|
|
|
// Start the internal file writer
|
|
if err := writer.Start(); err != nil {
|
|
return nil, fmt.Errorf("failed to start file writer: %w", err)
|
|
}
|
|
|
|
// Buffer size for input channel
|
|
// TODO: Centralized constant file in core package
|
|
bufferSize := int64(1000)
|
|
if bufSize, ok := options["buffer_size"].(int64); ok && bufSize > 0 {
|
|
bufferSize = bufSize
|
|
}
|
|
|
|
fs := &FileSink{
|
|
input: make(chan core.LogEntry, bufferSize),
|
|
writer: writer,
|
|
done: make(chan struct{}),
|
|
startTime: time.Now(),
|
|
logger: logger,
|
|
formatter: formatter,
|
|
}
|
|
fs.lastProcessed.Store(time.Time{})
|
|
|
|
return fs, nil
|
|
}
|
|
|
|
func (fs *FileSink) Input() chan<- core.LogEntry {
|
|
return fs.input
|
|
}
|
|
|
|
func (fs *FileSink) Start(ctx context.Context) error {
|
|
go fs.processLoop(ctx)
|
|
fs.logger.Info("msg", "File sink started", "component", "file_sink")
|
|
return nil
|
|
}
|
|
|
|
func (fs *FileSink) Stop() {
|
|
fs.logger.Info("msg", "Stopping file sink")
|
|
close(fs.done)
|
|
|
|
// Shutdown the writer with timeout
|
|
if err := fs.writer.Shutdown(2 * time.Second); err != nil {
|
|
fs.logger.Error("msg", "Error shutting down file writer",
|
|
"component", "file_sink",
|
|
"error", err)
|
|
}
|
|
|
|
fs.logger.Info("msg", "File sink stopped")
|
|
}
|
|
|
|
func (fs *FileSink) GetStats() SinkStats {
|
|
lastProc, _ := fs.lastProcessed.Load().(time.Time)
|
|
|
|
return SinkStats{
|
|
Type: "file",
|
|
TotalProcessed: fs.totalProcessed.Load(),
|
|
StartTime: fs.startTime,
|
|
LastProcessed: lastProc,
|
|
Details: map[string]any{},
|
|
}
|
|
}
|
|
|
|
func (fs *FileSink) processLoop(ctx context.Context) {
|
|
for {
|
|
select {
|
|
case entry, ok := <-fs.input:
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
fs.totalProcessed.Add(1)
|
|
fs.lastProcessed.Store(time.Now())
|
|
|
|
// Format using the formatter instead of fmt.Sprintf
|
|
formatted, err := fs.formatter.Format(entry)
|
|
if err != nil {
|
|
fs.logger.Error("msg", "Failed to format log entry",
|
|
"component", "file_sink",
|
|
"error", err)
|
|
continue
|
|
}
|
|
|
|
// Convert to string to prevent hex encoding of []byte by log package
|
|
// Strip new line, writer adds it
|
|
message := string(bytes.TrimSuffix(formatted, []byte{'\n'}))
|
|
fs.writer.Message(message)
|
|
|
|
case <-ctx.Done():
|
|
return
|
|
case <-fs.done:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (fs *FileSink) SetAuth(auth *config.AuthConfig) {
|
|
// Authentication does not apply to file sink
|
|
} |