view.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. package detail
  2. import (
  3. "fmt"
  4. "sort"
  5. "strings"
  6. "fbtop/internal/shared"
  7. "charm.land/lipgloss/v2"
  8. )
  9. var (
  10. titleStyle = lipgloss.NewStyle().Bold(true)
  11. okBadge = lipgloss.NewStyle().Foreground(lipgloss.Color("42")).Bold(true)
  12. warnBadge = lipgloss.NewStyle().Foreground(lipgloss.Color("214")).Bold(true)
  13. errBadge = lipgloss.NewStyle().Foreground(lipgloss.Color("196")).Bold(true)
  14. idleBadge = lipgloss.NewStyle().Foreground(lipgloss.Color("245")).Bold(true)
  15. hintStyle = lipgloss.NewStyle().Faint(true).Italic(true)
  16. panelStyle = lipgloss.NewStyle().Border(lipgloss.NormalBorder(), true).Padding(0, 1)
  17. )
  18. func Render(c shared.ComponentMetrics, prev shared.ComponentMetrics, width int) string {
  19. var b strings.Builder
  20. // Title with health badge
  21. h := shared.ComponentHealth(c, prev)
  22. badge := healthBadgeStyled(h)
  23. b.WriteString(fmt.Sprintf(" %s %s\n", badge, titleStyle.Render(c.ID+" ("+c.Kind.String()+")")))
  24. // Health explanation
  25. b.WriteString(" ")
  26. b.WriteString(healthExplanation(c, h))
  27. b.WriteString("\n\n")
  28. // Fields
  29. keys := make([]string, 0, len(c.Fields))
  30. for k := range c.Fields {
  31. keys = append(keys, k)
  32. }
  33. sort.Strings(keys)
  34. for _, k := range keys {
  35. v := c.Fields[k]
  36. indicator := ""
  37. if isProblemField(k, v, c.Kind) {
  38. indicator = errBadge.Render(" ← check this")
  39. }
  40. b.WriteString(fmt.Sprintf(" %-22s %d%s\n", k, v, indicator))
  41. }
  42. // Diagnostic hints
  43. hints := componentHints(c, h)
  44. if len(hints) > 0 {
  45. b.WriteString("\n")
  46. for _, h := range hints {
  47. b.WriteString(fmt.Sprintf(" %s\n", hintStyle.Render("💡 "+h)))
  48. }
  49. }
  50. return panelStyle.Width(width - 2).Render(b.String())
  51. }
  52. func healthBadgeStyled(h shared.Health) string {
  53. switch h {
  54. case shared.HealthOK:
  55. return okBadge.Render("● OK")
  56. case shared.HealthWarning:
  57. return warnBadge.Render("▲ WARNING")
  58. case shared.HealthError:
  59. return errBadge.Render("✖ ERROR")
  60. case shared.HealthIdle:
  61. return idleBadge.Render("○ IDLE")
  62. default:
  63. return "?"
  64. }
  65. }
  66. func healthExplanation(c shared.ComponentMetrics, h shared.Health) string {
  67. switch h {
  68. case shared.HealthOK:
  69. return okBadge.Render("Records flowing normally")
  70. case shared.HealthError:
  71. return errBadge.Render("Errors detected — see fields below")
  72. case shared.HealthWarning:
  73. return warnBadge.Render("Warnings detected — review below")
  74. case shared.HealthIdle:
  75. return idleBadge.Render("No records — check configuration")
  76. default:
  77. return ""
  78. }
  79. }
  80. func isProblemField(key string, val int64, kind shared.Kind) bool {
  81. if val <= 0 {
  82. return false
  83. }
  84. problemFields := map[string]bool{
  85. "errors": true,
  86. "retries": true,
  87. "retries_failed": true,
  88. "dropped_records": true,
  89. "drop_records": true,
  90. "drop_bytes": true,
  91. }
  92. return problemFields[key]
  93. }
  94. func componentHints(c shared.ComponentMetrics, h shared.Health) []string {
  95. var hints []string
  96. switch c.Kind {
  97. case shared.KindOutput:
  98. if c.Fields["errors"] > 0 {
  99. hints = append(hints, "Check output endpoint: TLS certs, credentials, auth tokens")
  100. hints = append(hints, "Verify destination host is reachable: check DNS and firewall")
  101. }
  102. if c.Fields["retries_failed"] > 0 {
  103. hints = append(hints, "Increase retry_limit or retry_window in output config")
  104. hints = append(hints, "Check if destination has rate limits or is rejecting connections")
  105. }
  106. if c.Fields["retries"] > 0 && c.Fields["errors"] == 0 {
  107. hints = append(hints, "Transient failures occurred — monitor or increase retry_limit")
  108. }
  109. if c.Fields["dropped_records"] > 0 {
  110. hints = append(hints, "Increase queue_limit or mem_buf_limit in output config")
  111. hints = append(hints, "Consider adding a filesystem-backed buffer (fsync: true)")
  112. }
  113. if c.Records() == 0 && h == shared.HealthIdle {
  114. hints = append(hints, "No records reaching this output — check filter match patterns")
  115. hints = append(hints, "Verify upstream input tags match the output match directive")
  116. }
  117. case shared.KindFilter:
  118. if c.Fields["drop_records"] > 0 {
  119. total := c.Records() + c.Fields["drop_records"]
  120. if total > 0 {
  121. ratio := float64(c.Fields["drop_records"]) / float64(total) * 100
  122. if ratio > 50 {
  123. hints = append(hints, fmt.Sprintf("Dropping %.0f%% of records — verify filter conditions", ratio))
  124. }
  125. }
  126. }
  127. if c.Records() == 0 && h == shared.HealthIdle {
  128. hints = append(hints, "Check filter match pattern: must match upstream output tags")
  129. hints = append(hints, "Verify record fields referenced in filter conditions exist")
  130. }
  131. case shared.KindInput:
  132. if c.Records() == 0 && h == shared.HealthIdle {
  133. hints = append(hints, "Check file path in input config — file may not exist or be empty")
  134. hints = append(hints, "Verify the input plugin is correctly configured (path, tag, parser)")
  135. hints = append(hints, "Check Fluent Bit logs for input plugin errors")
  136. }
  137. }
  138. return hints
  139. }