diag.go 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. package shared
  2. import "fmt"
  3. // Health represents per-component pipeline health.
  4. type Health int8
  5. const (
  6. HealthOK Health = iota // records flowing, no errors
  7. HealthWarning // retries, throughput anomaly
  8. HealthError // errors, dropped records, retries_failed
  9. HealthIdle // zero throughput — possible misconfig
  10. )
  11. func (h Health) String() string {
  12. return [...]string{"ok", "warn", "error", "idle"}[h]
  13. }
  14. // Badge returns a short colored indicator for table display.
  15. func (h Health) Badge() string {
  16. switch h {
  17. case HealthOK:
  18. return "●"
  19. case HealthWarning:
  20. return "▲"
  21. case HealthError:
  22. return "✖"
  23. case HealthIdle:
  24. return "○"
  25. default:
  26. return "?"
  27. }
  28. }
  29. // Diag is a single diagnostic message for the warnings panel.
  30. type Diag struct {
  31. Level Health
  32. CompID string // component ID, or "" for global
  33. Kind Kind // component kind
  34. Message string
  35. Hint string // suggested config area to check
  36. }
  37. // AnalyzeDiagnostics inspects a State and produces actionable diagnostics.
  38. func AnalyzeDiagnostics(s State, prevComps []ComponentMetrics) []Diag {
  39. var diags []Diag
  40. prevMap := make(map[string]ComponentMetrics, len(prevComps))
  41. for _, c := range prevComps {
  42. prevMap[c.ID] = c
  43. }
  44. for _, c := range s.Comps {
  45. diags = append(diags, componentDiags(c, prevMap[c.ID])...)
  46. }
  47. // Global diagnostics
  48. totalOut, totalIn := flowTotals(s.Comps)
  49. if totalIn > 0 && totalOut == 0 {
  50. diags = append(diags, Diag{
  51. Level: HealthError,
  52. Message: "No output records — pipeline is blocked or outputs misconfigured",
  53. Hint: "Check output plugin destination, auth, and network connectivity",
  54. })
  55. }
  56. return diags
  57. }
  58. // ComponentHealth returns the health status for a single component.
  59. func ComponentHealth(c ComponentMetrics, prev ComponentMetrics) Health {
  60. switch c.Kind {
  61. case KindOutput:
  62. if c.Fields["errors"] > 0 || c.Fields["retries_failed"] > 0 {
  63. return HealthError
  64. }
  65. if c.Fields["retries"] > 0 || c.Fields["dropped_records"] > 0 {
  66. return HealthWarning
  67. }
  68. if c.Records() == 0 {
  69. return HealthIdle
  70. }
  71. case KindFilter:
  72. total := c.Records() + c.Fields["drop_records"]
  73. if total > 0 {
  74. dropRatio := float64(c.Fields["drop_records"]) / float64(total)
  75. if dropRatio > 0.5 {
  76. return HealthWarning
  77. }
  78. }
  79. if c.Records() == 0 {
  80. return HealthIdle
  81. }
  82. case KindInput:
  83. if c.Records() == 0 {
  84. return HealthIdle
  85. }
  86. }
  87. return HealthOK
  88. }
  89. // PipelineSummary is a high-level summary for the header.
  90. type PipelineSummary struct {
  91. TotalInputs int
  92. TotalFilters int
  93. TotalOutputs int
  94. TotalErrors int64
  95. TotalRetries int64
  96. TotalDropped int64
  97. InputRec int64
  98. OutputRec int64
  99. HasErrors bool
  100. HasWarnings bool
  101. Flowing bool
  102. }
  103. func SummarizePipeline(comps []ComponentMetrics) PipelineSummary {
  104. var ps PipelineSummary
  105. for _, c := range comps {
  106. switch c.Kind {
  107. case KindInput:
  108. ps.TotalInputs++
  109. ps.InputRec += c.Records()
  110. case KindFilter:
  111. ps.TotalFilters++
  112. case KindOutput:
  113. ps.TotalOutputs++
  114. ps.OutputRec += c.Records()
  115. ps.TotalErrors += c.Fields["errors"]
  116. ps.TotalRetries += c.Fields["retries"]
  117. ps.TotalDropped += c.Fields["dropped_records"]
  118. }
  119. }
  120. ps.HasErrors = ps.TotalErrors > 0
  121. ps.HasWarnings = ps.TotalRetries > 0 || ps.TotalDropped > 0
  122. ps.Flowing = ps.InputRec > 0 && ps.OutputRec > 0
  123. return ps
  124. }
  125. func componentDiags(c, prev ComponentMetrics) []Diag {
  126. var diags []Diag
  127. h := ComponentHealth(c, prev)
  128. switch h {
  129. case HealthError:
  130. if c.Kind == KindOutput {
  131. if e := c.Fields["errors"]; e > 0 {
  132. diags = append(diags, Diag{
  133. Level: HealthError,
  134. CompID: c.ID,
  135. Kind: c.Kind,
  136. Message: fmt.Sprintf("%d processing errors", e),
  137. Hint: "Check output destination availability, TLS certs, and credentials",
  138. })
  139. }
  140. if rf := c.Fields["retries_failed"]; rf > 0 {
  141. diags = append(diags, Diag{
  142. Level: HealthError,
  143. CompID: c.ID,
  144. Kind: c.Kind,
  145. Message: fmt.Sprintf("%d failed retries", rf),
  146. Hint: "Destination may be unreachable — check network, DNS, and firewall rules",
  147. })
  148. }
  149. }
  150. case HealthWarning:
  151. if c.Kind == KindOutput {
  152. if r := c.Fields["retries"]; r > 0 {
  153. diags = append(diags, Diag{
  154. Level: HealthWarning,
  155. CompID: c.ID,
  156. Kind: c.Kind,
  157. Message: fmt.Sprintf("%d retries", r),
  158. Hint: "Transient failures — monitor or increase retry_limit in output config",
  159. })
  160. }
  161. if d := c.Fields["dropped_records"]; d > 0 {
  162. diags = append(diags, Diag{
  163. Level: HealthWarning,
  164. CompID: c.ID,
  165. Kind: c.Kind,
  166. Message: fmt.Sprintf("%d records dropped", d),
  167. Hint: "Check queue_limit / mem_buf_limit in output config",
  168. })
  169. }
  170. }
  171. if c.Kind == KindFilter {
  172. total := c.Records() + c.Fields["drop_records"]
  173. if total > 0 && float64(c.Fields["drop_records"])/float64(total) > 0.5 {
  174. diags = append(diags, Diag{
  175. Level: HealthWarning,
  176. CompID: c.ID,
  177. Kind: c.Kind,
  178. Message: fmt.Sprintf("dropping >%d%% of records", int(float64(c.Fields["drop_records"])/float64(total)*100)),
  179. Hint: "Verify filter conditions — high drop rate may indicate misconfigured rules",
  180. })
  181. }
  182. }
  183. case HealthIdle:
  184. switch c.Kind {
  185. case KindInput:
  186. diags = append(diags, Diag{
  187. Level: HealthIdle,
  188. CompID: c.ID,
  189. Kind: c.Kind,
  190. Message: "No records collected",
  191. Hint: "Check path/tag/source in input config — file may not exist or be empty",
  192. })
  193. case KindOutput:
  194. diags = append(diags, Diag{
  195. Level: HealthIdle,
  196. CompID: c.ID,
  197. Kind: c.Kind,
  198. Message: "No records processed",
  199. Hint: "Upstream may be blocked — verify filters are not dropping all records",
  200. })
  201. case KindFilter:
  202. diags = append(diags, Diag{
  203. Level: HealthIdle,
  204. CompID: c.ID,
  205. Kind: c.Kind,
  206. Message: "No records passed through",
  207. Hint: "Check filter match pattern and upstream output tags",
  208. })
  209. }
  210. }
  211. return diags
  212. }
  213. func flowTotals(comps []ComponentMetrics) (in, out int64) {
  214. for _, c := range comps {
  215. switch c.Kind {
  216. case KindInput:
  217. in += c.Records()
  218. case KindOutput:
  219. out += c.Records()
  220. }
  221. }
  222. return
  223. }