v0.33.0 tui tap for live view and console enhancements

This commit is contained in:
lixen committed 2026-10-10 21:32:08 -04:00
1 parent a1e9d21d06
commit 87f5576ccf
41 files changed
+4606 -449

No files matched your search

+51 -62
View File
@@ -21,13 +21,11 @@ import (
type Pipeline struct {
Config *config.PipelineConfig
// Components
Registry *Registry
Sources map[string]source.Source // Track instances by ID
Sources map[string]source.Source
Sinks map[string]sink.Sink
Sessions *session.Manager
// Pipeline flow
Flow *flow.Flow
Stats *PipelineStats
logger *log.Logger
@@ -35,7 +33,8 @@ type Pipeline struct {
// Sinks that stall dispatch rather than drop (core.CapBackpressure)
backpressure map[string]bool
// Runtime
tap atomic.Pointer[core.Tap] // a viewer's copy of what the flow decides
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
@@ -45,7 +44,6 @@ type Pipeline struct {
finishedOnce sync.Once
}
// PipelineStats contains runtime statistics for a pipeline
type PipelineStats struct {
StartTime time.Time
TotalEntriesDroppedBySink atomic.Uint64
@@ -54,18 +52,14 @@ type PipelineStats struct {
FlowStats map[string]any
}
// NewPipeline creates a new pipeline with registry support
func NewPipeline(
cfg *config.PipelineConfig,
logger *log.Logger,
) (*Pipeline, error) {
// Create pipeline context
pipelineCtx, pipelineCancel := context.WithCancel(context.Background())
// Create session manager with default timeout
sessionManager := session.NewManager(core.SessionDefaultMaxIdleTime)
// Create pipeline instance with registry
pipeline := &Pipeline{
Config: cfg,
Registry: NewRegistry(cfg.Name, logger),
@@ -81,17 +75,14 @@ func NewPipeline(
finished: make(chan struct{}),
}
// Create flow processor
flowProcessor, err := flow.NewFlow(cfg.Flow, logger)
if err != nil {
// If flow fails, stop session manager
pipelineCancel()
sessionManager.Stop()
return nil, fmt.Errorf("failed to create flow processor: %w", err)
}
pipeline.Flow = flowProcessor
// Initialize sources and sinks
if err := pipeline.initializeComponents(); err != nil {
pipelineCancel()
sessionManager.Stop()
@@ -102,10 +93,8 @@ func NewPipeline(
}
func (p *Pipeline) initializeComponents() error {
// Create sources based on plugin config if available
if len(p.Config.PluginSources) > 0 {
for _, srcCfg := range p.Config.PluginSources {
// Create session proxy for this source instance
sessionProxy := session.NewProxy(p.Sessions, srcCfg.ID)
src, err := p.Registry.CreateSource(
@@ -116,12 +105,11 @@ func (p *Pipeline) initializeComponents() error {
sessionProxy,
)
if err != nil {
return err // the registry names the plugin
return p.part("source", srcCfg.ID, err) // the registry names the plugin
}
// Check and inject capabilities using core interfaces
if err := p.initSourceCapabilities(src, srcCfg); err != nil {
return fmt.Errorf("failed to initiate capabilities for source %s: %w", srcCfg.ID, err)
return p.part("source", srcCfg.ID, fmt.Errorf("failed to initiate capabilities for source %s: %w", srcCfg.ID, err))
}
p.Sources[srcCfg.ID] = src
@@ -130,10 +118,8 @@ func (p *Pipeline) initializeComponents() error {
return fmt.Errorf("no plugin sources defined")
}
// Create sinks based on plugin config if available
if len(p.Config.PluginSinks) > 0 {
for _, sinkCfg := range p.Config.PluginSinks {
// Create session proxy for this sink instance
sessionProxy := session.NewProxy(p.Sessions, sinkCfg.ID)
snk, err := p.Registry.CreateSink(
@@ -144,12 +130,11 @@ func (p *Pipeline) initializeComponents() error {
sessionProxy,
)
if err != nil {
return err // the registry names the plugin
return p.part("sink", sinkCfg.ID, err) // the registry names the plugin
}
// Check and inject capabilities using core interfaces
if err := p.initSinkCapabilities(snk, sinkCfg); err != nil {
return fmt.Errorf("failed to initiate capabilities for sink %s: %w", sinkCfg.ID, err)
return p.part("sink", sinkCfg.ID, fmt.Errorf("failed to initiate capabilities for sink %s: %w", sinkCfg.ID, err))
}
p.Sinks[sinkCfg.ID] = snk
@@ -161,24 +146,34 @@ func (p *Pipeline) initializeComponents() error {
return nil
}
// initSourceCapabilities checks and injects optional capabilities
// PartError is a source's or sink's error, which names its part for a screen
// to place; its text is the error's own
type PartError struct {
Pipeline, Role, ID string
Err error
}
func (e *PartError) Error() string { return e.Err.Error() }
func (e *PartError) Unwrap() error { return e.Err }
func (p *Pipeline) part(role, id string, err error) error {
return &PartError{p.Config.Name, role, id, err}
}
func (p *Pipeline) initSourceCapabilities(s source.Source, cfg config.PluginSourceConfig) error {
// Initiate and activate source capabilities
var hasTLS, hasAuth bool
for _, c := range s.Capabilities() {
switch c {
// Network capabilities
case core.CapNetLimit:
continue // No-op for now, placeholder
continue
case core.CapTLS, core.CapProxyTLS:
hasTLS = true
case core.CapAuth:
hasAuth = true
// Session capabilities
case core.CapSessionAware:
case core.CapMultiSession:
continue // TODO
continue
default:
return fmt.Errorf("unknown capability type: %s", c)
@@ -202,27 +197,23 @@ func checkAuthCapability(hasTLS, hasAuth bool) error {
return nil
}
// initSinkCapabilities checks and injects optional capabilities
func (p *Pipeline) initSinkCapabilities(s sink.Sink, cfg config.PluginSinkConfig) error {
// Initiate and activate sink capabilities
var hasTLS, hasAuth bool
for _, c := range s.Capabilities() {
switch c {
case core.CapBackpressure:
p.backpressure[cfg.ID] = true
// Network capabilities
case core.CapNetLimit:
continue // No-op for now, placeholder
continue
case core.CapTLS, core.CapProxyTLS:
hasTLS = true
case core.CapAuth:
hasAuth = true
// Session capabilities
case core.CapSessionAware:
case core.CapMultiSession:
continue // TODO
continue
default:
return fmt.Errorf("unknown capability type: %s", c)
@@ -238,19 +229,23 @@ func (p *Pipeline) initSinkCapabilities(s sink.Sink, cfg config.PluginSinkConfig
// run connects the subscribed sources through the flow to the sinks. It
// returns once every source channel has closed, at Stop or at end of input.
func (p *Pipeline) run(subscriptions []<-chan core.LogEntry) {
func (p *Pipeline) run(ids []string, subscriptions []<-chan core.LogEntry) {
defer p.wg.Done()
defer p.finish()
defer p.logger.Info("msg", "Pipeline processing loop stopped", "pipeline", p.Config.Name)
var componentWg sync.WaitGroup
for _, ch := range subscriptions {
for i, ch := range subscriptions {
componentWg.Add(1)
go func() {
defer componentWg.Done()
// Range drains in-flight entries once the source closes the channel
for entry := range ch {
if event, passed := p.Flow.Process(entry); passed {
event, stage, index := p.Flow.Process(entry)
if t := p.tap.Load(); t != nil {
t.Offer(core.Tapped{Pipeline: p.Config.Name, Source: ids[i], Stage: stage, Index: index, Event: event})
}
if stage == "" {
p.dispatch(event)
}
}
@@ -270,6 +265,9 @@ func (p *Pipeline) run(subscriptions []<-chan core.LogEntry) {
if !ok {
return
}
if t := p.tap.Load(); t != nil {
t.Offer(core.Tapped{Pipeline: p.Config.Name, Event: event})
}
p.dispatch(event)
case <-hbCtx.Done():
return
@@ -303,13 +301,16 @@ func (p *Pipeline) dispatch(event core.TransportEvent) {
}
}
// SetTap offers what the flow decides to t from the next entry on; nil stops
func (p *Pipeline) SetTap(t *core.Tap) { p.tap.Store(t) }
func (p *Pipeline) finish() { p.finishedOnce.Do(func() { close(p.finished) }) }
// Finished is closed when every source has ended, or the pipeline shut down.
// Only a finite source (console at end of input) ends without Stop.
func (p *Pipeline) Finished() <-chan struct{} { return p.finished }
// Start starts the pipeline operation and all its components including flow, sources, and sinks
// Start starts the sinks before the sources, so the first entry has somewhere to go
func (p *Pipeline) Start() error {
if !p.running.CompareAndSwap(false, true) {
return fmt.Errorf("pipeline %s is already running", p.Config.Name)
@@ -319,33 +320,34 @@ func (p *Pipeline) Start() error {
p.ctx, p.cancel = context.WithCancel(context.Background())
p.halt = make(chan struct{})
// Start all sinks
for id, s := range p.Sinks {
if err := s.Start(p.ctx); err != nil {
return fmt.Errorf("failed to start sink %s: %w", id, err)
return p.part("sink", id, fmt.Errorf("failed to start sink %s: %w", id, err))
}
}
// Subscribed before Start: a source publishes from its first entry on
// Subscribed before Start: a source publishes from its first entry on. One
// loop keeps ids beside their channels; map order differs between loops.
ids := make([]string, 0, len(p.Sources))
subscriptions := make([]<-chan core.LogEntry, 0, len(p.Sources))
for _, src := range p.Sources {
subscriptions = append(subscriptions, src.Subscribe())
for id, src := range p.Sources {
ids, subscriptions = append(ids, id), append(subscriptions, src.Subscribe())
}
for id, src := range p.Sources {
if err := src.Start(); err != nil {
return fmt.Errorf("failed to start source %s: %w", id, err)
return p.part("source", id, fmt.Errorf("failed to start source %s: %w", id, err))
}
}
// Start the central processing loop
p.Stats.StartTime = time.Now()
p.wg.Add(1)
go p.run(subscriptions)
go p.run(ids, subscriptions)
return nil
}
// Stop stops the pipeline operation and all its components including flow, sources, and sinks
// Stop stops the sources, lets the run loop drain, then stops the sinks, so
// entries in flight reach them
func (p *Pipeline) Stop() error {
if !p.running.CompareAndSwap(true, false) {
return fmt.Errorf("pipeline %s is not running", p.Config.Name)
@@ -354,7 +356,6 @@ func (p *Pipeline) Stop() error {
p.logger.Info("msg", "Stopping pipeline", "pipeline", p.Config.Name)
close(p.halt)
// 1. Stop all sources concurrently to halt new data ingress and close their channels
var sourceWg sync.WaitGroup
for _, src := range p.Sources {
sourceWg.Add(1)
@@ -365,10 +366,8 @@ func (p *Pipeline) Stop() error {
}
sourceWg.Wait()
// 2. Wait for the run loop to finish processing and sending all in-flight data
p.wg.Wait()
// 3. Stop all sinks concurrently now that no new data will be sent
var sinkWg sync.WaitGroup
for _, s := range p.Sinks {
sinkWg.Add(1)
@@ -384,20 +383,18 @@ func (p *Pipeline) Stop() error {
return nil
}
// Shutdown gracefully stops the pipeline and all its components, deinitializing them for app shutdown or complete pipeline removal by service
// Shutdown stops the pipeline for good, with its session manager
func (p *Pipeline) Shutdown() {
p.logger.Info("msg", "Shutting down pipeline",
"component", "pipeline",
"pipeline", p.Config.Name)
// Ensure the pipeline is stopped before shutting down
if p.running.Load() {
if err := p.Stop(); err != nil {
p.logger.Error("msg", "Error stopping pipeline during shutdown", "error", err)
}
}
// Stop long-running components
if p.Sessions != nil {
p.Sessions.Stop()
}
@@ -408,10 +405,8 @@ func (p *Pipeline) Shutdown() {
"pipeline", p.Config.Name)
}
// GetStats returns a map of pipeline statistics
func (p *Pipeline) GetStats() map[string]any {
// Recovery to handle concurrent access during shutdown
// When service is shutting down, sources/sinks might be nil or partially stopped
// Sources and sinks may be partly stopped while the service shuts down
defer func() {
if r := recover(); r != nil {
p.logger.Error("msg", "Panic getting pipeline stats",
@@ -420,7 +415,6 @@ func (p *Pipeline) GetStats() map[string]any {
}
}()
// 1. Live collect source stats
sources := make([]map[string]any, 0, len(p.Sources))
for _, src := range p.Sources {
if src == nil {
@@ -438,7 +432,6 @@ func (p *Pipeline) GetStats() map[string]any {
})
}
// 2. Live collect sink stats
sinks := make([]map[string]any, 0, len(p.Sinks))
for _, snk := range p.Sinks {
if snk == nil {
@@ -456,7 +449,6 @@ func (p *Pipeline) GetStats() map[string]any {
})
}
// 3. Collect flow stats and calculate filtered total
var flowStats map[string]any
var totalFiltered uint64
var totalProcessed uint64
@@ -464,12 +456,10 @@ func (p *Pipeline) GetStats() map[string]any {
if p.Flow != nil {
flowStats = p.Flow.GetStats()
// Map the top-level processed counter directly from Flow's source of truth
if tp, ok := flowStats["total_processed"].(uint64); ok {
totalProcessed = tp
}
// Calculate total dropped specifically by the filter chain
if filters, ok := flowStats["filters"].(map[string]any); ok {
if totalPassed, ok := filters["total_passed"].(uint64); ok {
if tProc, ok := filters["total_processed"].(uint64); ok {
@@ -479,7 +469,6 @@ func (p *Pipeline) GetStats() map[string]any {
}
}
// 4. Calculate Uptime
var uptime int
if p.running.Load() && !p.Stats.StartTime.IsZero() {
uptime = int(time.Since(p.Stats.StartTime).Seconds())