5238f279db
- Add StreamStatus type (relay/common) to track stream end reason (done/timeout/client_gone/scanner_error/eof/panic/ping_fail) and accumulate soft errors during streaming via sync.Once + sync.Mutex. - Add StreamResult (relay/helper) as the callback interface: adapters call sr.Error() for soft errors, sr.Stop() for fatal, sr.Done() for normal completion. No early-return problem — multiple errors per chunk are naturally supported. - Refactor StreamScannerHandler callback from func(string) bool to func(string, *StreamResult). All 9 channel adapters updated. - Write stream_status into log other JSON field (admin-only) with status ok/error, end_reason, error_count, and error messages. - Frontend: display stream status in log detail expansion for admins.
113 lines
2.2 KiB
Go
113 lines
2.2 KiB
Go
package common
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type StreamEndReason string
|
|
|
|
const (
|
|
StreamEndReasonNone StreamEndReason = ""
|
|
StreamEndReasonDone StreamEndReason = "done"
|
|
StreamEndReasonTimeout StreamEndReason = "timeout"
|
|
StreamEndReasonClientGone StreamEndReason = "client_gone"
|
|
StreamEndReasonScannerErr StreamEndReason = "scanner_error"
|
|
StreamEndReasonHandlerStop StreamEndReason = "handler_stop"
|
|
StreamEndReasonEOF StreamEndReason = "eof"
|
|
StreamEndReasonPanic StreamEndReason = "panic"
|
|
StreamEndReasonPingFail StreamEndReason = "ping_fail"
|
|
)
|
|
|
|
const maxStreamErrorEntries = 20
|
|
|
|
type StreamErrorEntry struct {
|
|
Message string
|
|
Timestamp time.Time
|
|
}
|
|
|
|
type StreamStatus struct {
|
|
EndReason StreamEndReason
|
|
EndError error
|
|
endOnce sync.Once
|
|
|
|
mu sync.Mutex
|
|
Errors []StreamErrorEntry
|
|
ErrorCount int
|
|
}
|
|
|
|
func NewStreamStatus() *StreamStatus {
|
|
return &StreamStatus{}
|
|
}
|
|
|
|
func (s *StreamStatus) SetEndReason(reason StreamEndReason, err error) {
|
|
if s == nil {
|
|
return
|
|
}
|
|
s.endOnce.Do(func() {
|
|
s.EndReason = reason
|
|
s.EndError = err
|
|
})
|
|
}
|
|
|
|
func (s *StreamStatus) RecordError(msg string) {
|
|
if s == nil {
|
|
return
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.ErrorCount++
|
|
if len(s.Errors) < maxStreamErrorEntries {
|
|
s.Errors = append(s.Errors, StreamErrorEntry{
|
|
Message: msg,
|
|
Timestamp: time.Now(),
|
|
})
|
|
}
|
|
}
|
|
|
|
func (s *StreamStatus) HasErrors() bool {
|
|
if s == nil {
|
|
return false
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return s.ErrorCount > 0
|
|
}
|
|
|
|
func (s *StreamStatus) TotalErrorCount() int {
|
|
if s == nil {
|
|
return 0
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return s.ErrorCount
|
|
}
|
|
|
|
func (s *StreamStatus) IsNormalEnd() bool {
|
|
if s == nil {
|
|
return true
|
|
}
|
|
return s.EndReason == StreamEndReasonDone ||
|
|
s.EndReason == StreamEndReasonEOF ||
|
|
s.EndReason == StreamEndReasonHandlerStop
|
|
}
|
|
|
|
func (s *StreamStatus) Summary() string {
|
|
if s == nil {
|
|
return "StreamStatus<nil>"
|
|
}
|
|
b := &strings.Builder{}
|
|
fmt.Fprintf(b, "reason=%s", s.EndReason)
|
|
if s.EndError != nil {
|
|
fmt.Fprintf(b, " end_error=%q", s.EndError.Error())
|
|
}
|
|
s.mu.Lock()
|
|
if s.ErrorCount > 0 {
|
|
fmt.Fprintf(b, " soft_errors=%d", s.ErrorCount)
|
|
}
|
|
s.mu.Unlock()
|
|
return b.String()
|
|
}
|