249 lines
6.2 KiB
Go
249 lines
6.2 KiB
Go
// FILE: logwisp/src/internal/service/service.go
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"logwisp/src/internal/config"
|
|
"logwisp/src/internal/core"
|
|
"logwisp/src/internal/format"
|
|
"logwisp/src/internal/sink"
|
|
"logwisp/src/internal/source"
|
|
|
|
"github.com/lixenwraith/log"
|
|
)
|
|
|
|
// Service manages a collection of log processing pipelines.
|
|
type Service struct {
|
|
pipelines map[string]*Pipeline
|
|
mu sync.RWMutex
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
wg sync.WaitGroup
|
|
logger *log.Logger
|
|
}
|
|
|
|
// NewService creates a new, empty service.
|
|
func NewService(ctx context.Context, logger *log.Logger) *Service {
|
|
serviceCtx, cancel := context.WithCancel(ctx)
|
|
return &Service{
|
|
pipelines: make(map[string]*Pipeline),
|
|
ctx: serviceCtx,
|
|
cancel: cancel,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// GetPipeline returns a pipeline by its name.
|
|
func (s *Service) GetPipeline(name string) (*Pipeline, error) {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
pipeline, exists := s.pipelines[name]
|
|
if !exists {
|
|
return nil, fmt.Errorf("pipeline '%s' not found", name)
|
|
}
|
|
return pipeline, nil
|
|
}
|
|
|
|
// ListPipelines returns the names of all currently managed pipelines.
|
|
func (s *Service) ListPipelines() []string {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
names := make([]string, 0, len(s.pipelines))
|
|
for name := range s.pipelines {
|
|
names = append(names, name)
|
|
}
|
|
return names
|
|
}
|
|
|
|
// RemovePipeline stops and removes a pipeline from the service.
|
|
func (s *Service) RemovePipeline(name string) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
pipeline, exists := s.pipelines[name]
|
|
if !exists {
|
|
err := fmt.Errorf("pipeline '%s' not found", name)
|
|
s.logger.Warn("msg", "Cannot remove non-existent pipeline",
|
|
"component", "service",
|
|
"pipeline", name,
|
|
"error", err)
|
|
return err
|
|
}
|
|
|
|
s.logger.Info("msg", "Removing pipeline", "pipeline", name)
|
|
pipeline.Shutdown()
|
|
delete(s.pipelines, name)
|
|
return nil
|
|
}
|
|
|
|
// Shutdown gracefully stops all pipelines managed by the service.
|
|
func (s *Service) Shutdown() {
|
|
s.logger.Info("msg", "Service shutdown initiated")
|
|
|
|
s.mu.Lock()
|
|
pipelines := make([]*Pipeline, 0, len(s.pipelines))
|
|
for _, pipeline := range s.pipelines {
|
|
pipelines = append(pipelines, pipeline)
|
|
}
|
|
s.mu.Unlock()
|
|
|
|
// Stop all pipelines concurrently
|
|
var wg sync.WaitGroup
|
|
for _, pipeline := range pipelines {
|
|
wg.Add(1)
|
|
go func(p *Pipeline) {
|
|
defer wg.Done()
|
|
p.Shutdown()
|
|
}(pipeline)
|
|
}
|
|
wg.Wait()
|
|
|
|
s.cancel()
|
|
s.wg.Wait()
|
|
|
|
s.logger.Info("msg", "Service shutdown complete")
|
|
}
|
|
|
|
// GetGlobalStats returns statistics for all pipelines.
|
|
func (s *Service) GetGlobalStats() map[string]any {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
stats := map[string]any{
|
|
"pipelines": make(map[string]any),
|
|
"total_pipelines": len(s.pipelines),
|
|
}
|
|
|
|
for name, pipeline := range s.pipelines {
|
|
stats["pipelines"].(map[string]any)[name] = pipeline.GetStats()
|
|
}
|
|
|
|
return stats
|
|
}
|
|
|
|
// wirePipeline connects a pipeline's sources to its sinks through its filter chain.
|
|
func (s *Service) wirePipeline(p *Pipeline) {
|
|
// For each source, subscribe and process entries
|
|
for _, src := range p.Sources {
|
|
srcChan := src.Subscribe()
|
|
|
|
// Create a processing goroutine for this source
|
|
p.wg.Add(1)
|
|
go func(source source.Source, entries <-chan core.LogEntry) {
|
|
defer p.wg.Done()
|
|
|
|
// Panic recovery to prevent single source from crashing pipeline
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
s.logger.Error("msg", "Panic in pipeline processing",
|
|
"pipeline", p.Config.Name,
|
|
"source", source.GetStats().Type,
|
|
"panic", r)
|
|
|
|
// Ensure failed pipelines don't leave resources hanging
|
|
go func() {
|
|
s.logger.Warn("msg", "Shutting down pipeline due to panic",
|
|
"pipeline", p.Config.Name)
|
|
if err := s.RemovePipeline(p.Config.Name); err != nil {
|
|
s.logger.Error("msg", "Failed to remove panicked pipeline",
|
|
"pipeline", p.Config.Name,
|
|
"error", err)
|
|
}
|
|
}()
|
|
}
|
|
}()
|
|
|
|
for {
|
|
select {
|
|
case <-p.ctx.Done():
|
|
return
|
|
case entry, ok := <-entries:
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
p.Stats.TotalEntriesProcessed.Add(1)
|
|
|
|
// Apply pipeline rate limiter
|
|
if p.RateLimiter != nil {
|
|
if !p.RateLimiter.Allow(entry) {
|
|
p.Stats.TotalEntriesDroppedByRateLimit.Add(1)
|
|
continue // Drop the entry
|
|
}
|
|
}
|
|
|
|
// Apply filters if configured
|
|
if p.FilterChain != nil {
|
|
if !p.FilterChain.Apply(entry) {
|
|
p.Stats.TotalEntriesFiltered.Add(1)
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Send to all sinks
|
|
for _, sinkInst := range p.Sinks {
|
|
select {
|
|
case sinkInst.Input() <- entry:
|
|
case <-p.ctx.Done():
|
|
return
|
|
default:
|
|
// Drop if sink buffer is full, may flood logging for slow client
|
|
s.logger.Debug("msg", "Dropped log entry - sink buffer full",
|
|
"pipeline", p.Config.Name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}(src, srcChan)
|
|
}
|
|
}
|
|
|
|
// createSource is a factory function for creating a source instance from configuration.
|
|
func (s *Service) createSource(cfg *config.SourceConfig) (source.Source, error) {
|
|
switch cfg.Type {
|
|
case "file":
|
|
return source.NewFileSource(cfg.File, s.logger)
|
|
case "console":
|
|
return source.NewConsoleSource(cfg.Console, s.logger)
|
|
case "http":
|
|
return source.NewHTTPSource(cfg.HTTP, s.logger)
|
|
case "tcp":
|
|
return source.NewTCPSource(cfg.TCP, s.logger)
|
|
default:
|
|
return nil, fmt.Errorf("unknown source type: %s", cfg.Type)
|
|
}
|
|
}
|
|
|
|
// createSink is a factory function for creating a sink instance from configuration.
|
|
func (s *Service) createSink(cfg config.SinkConfig, formatter format.Formatter) (sink.Sink, error) {
|
|
|
|
switch cfg.Type {
|
|
case "http":
|
|
if cfg.HTTP == nil {
|
|
return nil, fmt.Errorf("HTTP sink configuration missing")
|
|
}
|
|
return sink.NewHTTPSink(cfg.HTTP, s.logger, formatter)
|
|
|
|
case "tcp":
|
|
if cfg.TCP == nil {
|
|
return nil, fmt.Errorf("TCP sink configuration missing")
|
|
}
|
|
return sink.NewTCPSink(cfg.TCP, s.logger, formatter)
|
|
|
|
case "http_client":
|
|
return sink.NewHTTPClientSink(cfg.HTTPClient, s.logger, formatter)
|
|
case "tcp_client":
|
|
return sink.NewTCPClientSink(cfg.TCPClient, s.logger, formatter)
|
|
case "file":
|
|
return sink.NewFileSink(cfg.File, s.logger, formatter)
|
|
case "console":
|
|
return sink.NewConsoleSink(cfg.Console, s.logger, formatter)
|
|
default:
|
|
return nil, fmt.Errorf("unknown sink type: %s", cfg.Type)
|
|
}
|
|
} |