package main import ( "context" "fmt" "time" "github.com/lixenwraith/logwisp/internal/service" ) // startStatusReporter starts a new status reporter for a service and returns its cancel function. func startStatusReporter(ctx context.Context, svc *service.Service) context.CancelFunc { reporterCtx, cancel := context.WithCancel(ctx) go statusReporter(svc, reporterCtx) logger.Debug("msg", "Started status reporter") return cancel } // statusReporter periodically logs the health and statistics of the service func statusReporter(service *service.Service, ctx context.Context) { ticker := time.NewTicker(30 * time.Second) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: if service == nil { logger.Warn("msg", "Status reporter: service is nil", "component", "status_reporter") return } // Safely get stats with recovery func() { defer func() { if r := recover(); r != nil { logger.Error("msg", "Panic in status reporter", "component", "status_reporter", "panic", r) } }() stats := service.GetGlobalStats() totalPipelines, ok := stats["total_pipelines"].(int) if !ok || totalPipelines == 0 { logger.Warn("msg", "No active pipelines in status report", "component", "status_reporter") return } // Log service-level summary logger.Debug("msg", "Status report", "component", "status_reporter", "active_pipelines", totalPipelines, "time", time.Now().Format("15:04:05")) // Log each pipeline's stats recursively if pipelines, ok := stats["pipelines"].(map[string]any); ok { for name, pipelineStats := range pipelines { logStats("Pipeline status", name, pipelineStats) } } }() } } } // logStats recursively logs statistics with automatic field extraction func logStats(msg string, name string, stats any) { if statsMap, ok := stats.(map[string]any); ok { logger.Debug(statsFields(msg, name, statsMap)...) // Recursively log nested structures with detail for key, value := range statsMap { switch v := value.(type) { case map[string]any: // Log nested component stats if key == "flow" || key == "rate_limiter" || key == "filters" { logStats(fmt.Sprintf("%s %s", name, key), key, v) } case []map[string]any: // Log array items (sources, sinks, filters) for i, item := range v { if itemName, ok := item["id"].(string); ok { logStats(fmt.Sprintf("%s %s", name, key), itemName, item) } else { logStats(fmt.Sprintf("%s %s", name, key), fmt.Sprintf("%s[%d]", key, i), item) } } } } } } // statsFields flattens one stats map into log fields. A plugin's details map // is spread into the line: its auth and TLS rejection counters live there. func statsFields(msg, name string, stats map[string]any) []any { fields := []any{"msg", msg, "name", name} add := func(key string, value any) { switch v := value.(type) { case string, bool, int, int64, uint64, float64: fields = append(fields, key, v) case time.Time: if !v.IsZero() { fields = append(fields, key, v.Format(time.RFC3339)) } case map[string]any: // For nested maps, log summary counts if they contain arrays/maps if count := getItemCount(v); count > 0 { fields = append(fields, fmt.Sprintf("%s_count", key), count) } case []any, []map[string]any: fields = append(fields, fmt.Sprintf("%s_count", key), getArrayLength(value)) } } for key, value := range stats { if details, ok := value.(map[string]any); ok && key == "details" { for k, v := range details { add(k, v) } continue } add(key, value) } return fields } // getItemCount returns the count of items in a map (for nested structures) func getItemCount(m map[string]any) int { for _, v := range m { switch v.(type) { case []any: return len(v.([]any)) case []map[string]any: return len(v.([]map[string]any)) } } return 0 } // getArrayLength safely gets the length of various array types func getArrayLength(v any) int { switch arr := v.(type) { case []any: return len(arr) case []map[string]any: return len(arr) default: return 0 } }