diagnostics.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. package cmd
  2. import (
  3. "errors"
  4. "fmt"
  5. "net/http"
  6. _ "net/http/pprof"
  7. "os"
  8. "path/filepath"
  9. "runtime"
  10. "runtime/pprof"
  11. "runtime/trace"
  12. "strings"
  13. "github.com/zricethezav/gitleaks/v8/logging"
  14. )
  15. // DiagnosticsManager manages various types of diagnostics
  16. type DiagnosticsManager struct {
  17. Enabled bool
  18. DiagTypes []string
  19. OutputDir string
  20. cpuProfile *os.File
  21. memProfile string
  22. traceProfile *os.File
  23. }
  24. // NewDiagnosticsManager creates a new DiagnosticsManager instance
  25. func NewDiagnosticsManager(diagnosticsFlag string, diagnosticsDir string) (*DiagnosticsManager, error) {
  26. if diagnosticsFlag == "" {
  27. return &DiagnosticsManager{Enabled: false}, nil
  28. }
  29. dm := &DiagnosticsManager{
  30. Enabled: true,
  31. DiagTypes: strings.Split(diagnosticsFlag, ","),
  32. OutputDir: diagnosticsDir,
  33. }
  34. if diagnosticsFlag == "http" {
  35. if len(diagnosticsDir) != 0 {
  36. return nil, errors.New("the diagnostics directory should not be set in http mode")
  37. }
  38. return dm, nil
  39. }
  40. // If no output directory is specified, use the current directory
  41. if dm.OutputDir == "" {
  42. var err error
  43. dm.OutputDir, err = os.Getwd()
  44. if err != nil {
  45. return nil, fmt.Errorf("failed to get current directory: %w", err)
  46. }
  47. logging.Debug().Msgf("No diagnostics directory specified, using current directory: %s", dm.OutputDir)
  48. }
  49. // Create the output directory if it doesn't exist
  50. if err := os.MkdirAll(dm.OutputDir, 0755); err != nil {
  51. return nil, fmt.Errorf("failed to create diagnostics directory: %w", err)
  52. }
  53. // Make sure the output directory is absolute
  54. if !filepath.IsAbs(dm.OutputDir) {
  55. absPath, err := filepath.Abs(dm.OutputDir)
  56. if err != nil {
  57. return nil, fmt.Errorf("failed to get absolute path for diagnostics directory: %w", err)
  58. }
  59. dm.OutputDir = absPath
  60. }
  61. logging.Debug().Msgf("Diagnostics enabled: %s", strings.Join(dm.DiagTypes, ","))
  62. logging.Debug().Msgf("Diagnostics output directory: %s", dm.OutputDir)
  63. return dm, nil
  64. }
  65. // StartDiagnostics starts all enabled diagnostics
  66. func (dm *DiagnosticsManager) StartDiagnostics() error {
  67. if !dm.Enabled {
  68. return nil
  69. }
  70. var err error
  71. for _, diagType := range dm.DiagTypes {
  72. diagType = strings.TrimSpace(diagType)
  73. switch diagType {
  74. case "cpu":
  75. if err = dm.StartCPUProfile(); err != nil {
  76. return err
  77. }
  78. case "mem":
  79. if err = dm.SetupMemoryProfile(); err != nil {
  80. return err
  81. }
  82. case "trace":
  83. if err = dm.StartTraceProfile(); err != nil {
  84. return err
  85. }
  86. case "http":
  87. if err = dm.StartHttpHandler(); err != nil {
  88. return err
  89. }
  90. default:
  91. logging.Warn().Msgf("Unknown diagnostics type: %s", diagType)
  92. }
  93. }
  94. return nil
  95. }
  96. // StopDiagnostics stops all started diagnostics
  97. func (dm *DiagnosticsManager) StopDiagnostics() {
  98. if !dm.Enabled {
  99. return
  100. }
  101. logging.Debug().Msg("Stopping diagnostics and writing profiling data...")
  102. for _, diagType := range dm.DiagTypes {
  103. diagType = strings.TrimSpace(diagType)
  104. switch diagType {
  105. case "cpu":
  106. dm.StopCPUProfile()
  107. case "mem":
  108. dm.WriteMemoryProfile()
  109. case "trace":
  110. dm.StopTraceProfile()
  111. case "http":
  112. // No need to stop the http one
  113. }
  114. }
  115. }
  116. func (dm *DiagnosticsManager) StartHttpHandler() error {
  117. if len(dm.DiagTypes) > 1 {
  118. return errors.New("other diagnostics modes should not be enabled when http mode is enabled")
  119. }
  120. go func() {
  121. logging.Error().Err(http.ListenAndServe("localhost:6060", nil)).Send()
  122. }()
  123. logging.Info().Str("url", "http://localhost:6060/debug/pprof/").Msg("Diagnostics server started")
  124. return nil
  125. }
  126. // StartCPUProfile starts CPU profiling
  127. func (dm *DiagnosticsManager) StartCPUProfile() error {
  128. cpuProfilePath := filepath.Join(dm.OutputDir, "cpu.pprof")
  129. f, err := os.Create(cpuProfilePath)
  130. if err != nil {
  131. return fmt.Errorf("could not create CPU profile at %s: %w", cpuProfilePath, err)
  132. }
  133. if err := pprof.StartCPUProfile(f); err != nil {
  134. _ = f.Close()
  135. return fmt.Errorf("could not start CPU profile: %w", err)
  136. }
  137. dm.cpuProfile = f
  138. return nil
  139. }
  140. // StopCPUProfile stops CPU profiling
  141. func (dm *DiagnosticsManager) StopCPUProfile() {
  142. if dm.cpuProfile != nil {
  143. pprof.StopCPUProfile()
  144. if err := dm.cpuProfile.Close(); err != nil {
  145. logging.Error().Err(err).Msg("Error closing CPU profile file")
  146. }
  147. logging.Info().Msgf("CPU profile written to: %s", dm.cpuProfile.Name())
  148. dm.cpuProfile = nil
  149. }
  150. }
  151. // SetupMemoryProfile sets up memory profiling to be written when StopDiagnostics is called
  152. func (dm *DiagnosticsManager) SetupMemoryProfile() error {
  153. memProfilePath := filepath.Join(dm.OutputDir, "mem.pprof")
  154. dm.memProfile = memProfilePath
  155. return nil
  156. }
  157. // WriteMemoryProfile writes the memory profile to disk
  158. func (dm *DiagnosticsManager) WriteMemoryProfile() {
  159. if dm.memProfile == "" {
  160. return
  161. }
  162. f, err := os.Create(dm.memProfile)
  163. if err != nil {
  164. logging.Error().Err(err).Msgf("Could not create memory profile at %s", dm.memProfile)
  165. return
  166. }
  167. // Get memory profile
  168. runtime.GC() // Run GC before taking the memory profile
  169. if err := pprof.WriteHeapProfile(f); err != nil {
  170. logging.Error().Err(err).Msg("Could not write memory profile")
  171. } else {
  172. logging.Info().Msgf("Memory profile written to: %s", dm.memProfile)
  173. }
  174. if err := f.Close(); err != nil {
  175. logging.Error().Err(err).Msg("Error closing memory profile file")
  176. }
  177. dm.memProfile = ""
  178. }
  179. // StartTraceProfile starts execution tracing
  180. func (dm *DiagnosticsManager) StartTraceProfile() error {
  181. traceProfilePath := filepath.Join(dm.OutputDir, "trace.out")
  182. f, err := os.Create(traceProfilePath)
  183. if err != nil {
  184. return fmt.Errorf("could not create trace profile at %s: %w", traceProfilePath, err)
  185. }
  186. if err := trace.Start(f); err != nil {
  187. _ = f.Close()
  188. return fmt.Errorf("could not start trace profile: %w", err)
  189. }
  190. dm.traceProfile = f
  191. return nil
  192. }
  193. // StopTraceProfile stops execution tracing
  194. func (dm *DiagnosticsManager) StopTraceProfile() {
  195. if dm.traceProfile != nil {
  196. trace.Stop()
  197. if err := dm.traceProfile.Close(); err != nil {
  198. logging.Error().Err(err).Msg("Error closing trace profile file")
  199. }
  200. logging.Info().Msgf("Trace profile written to: %s", dm.traceProfile.Name())
  201. dm.traceProfile = nil
  202. }
  203. }