github.com/Schaudge/grailbase@v0.0.0-20240223061707-44c758a471c0/diagnostic/dump/default.go (about) 1 // Copyright 2019 GRAIL, Inc. All rights reserved. 2 // Use of this source code is governed by the Apache 2.0 3 // license that can be found in the LICENSE file. 4 5 package dump 6 7 import ( 8 "archive/zip" 9 "context" 10 "encoding/json" 11 "expvar" 12 "fmt" 13 "io" 14 "os" 15 "path/filepath" 16 "runtime" 17 "runtime/pprof" 18 "strings" 19 "time" 20 21 "github.com/Schaudge/grailbase/log" 22 "github.com/shirou/gopsutil/cpu" 23 "github.com/shirou/gopsutil/load" 24 "github.com/shirou/gopsutil/mem" 25 ) 26 27 // DefaultRegistry is a default registry that has this process's GUID as its ID. 28 var DefaultRegistry = NewRegistry(readExec()) 29 30 // Register registers a new part to be included in the dump of the 31 // DefaultRegistry. name will become the filename of the part file in the dump 32 // tarball. f will be called to produce the contents of that file. 33 func Register(name string, f Func) { 34 DefaultRegistry.Register(name, f) 35 } 36 37 // WriteDump writes a dump of the default registry. 38 func WriteDump(ctx context.Context, pfx string, zw *zip.Writer) { 39 DefaultRegistry.WriteDump(ctx, pfx, zw) 40 } 41 42 // Name returns the name of the default registry. See (*Registry).Name. 43 func Name() string { 44 return DefaultRegistry.Name() 45 } 46 47 // readExec returns a sanitized version of the executable name, if it can be 48 // determined. If not, returns "unknown". 49 func readExec() string { 50 const unknown = "unknown" 51 execPath, err := os.Executable() 52 if err != nil { 53 return unknown 54 } 55 rawExec := filepath.Base(execPath) 56 var sanitized strings.Builder 57 for _, r := range rawExec { 58 if (r == '-' || 'a' <= r && r <= 'z') || ('0' <= r && r <= '9') { 59 sanitized.WriteRune(r) 60 } 61 } 62 if sanitized.Len() == 0 { 63 return unknown 64 } 65 return sanitized.String() 66 } 67 68 // shellQuote quotes a string to be used as an argument in an sh command line. 69 func shellQuote(s string) string { 70 // We wrap with single quotes, as they will work with any string except 71 // those with single quotes. We handle single quotes by tranforming them 72 // into "'\''" and letting the shell concatenate the strings back together. 73 return "'" + strings.Replace(s, "'", `'\''`, -1) + "'" 74 } 75 76 // dumpCmdline writes the command-line of the current execution. It writes it 77 // in a format that can be directly pasted into sh to be run. 78 func dumpCmdline(ctx context.Context, w io.Writer) error { 79 args := make([]string, len(os.Args)) 80 for i := range args { 81 args[i] = shellQuote(os.Args[i]) 82 } 83 _, err := io.WriteString(w, strings.Join(args, " ")) 84 return err 85 } 86 87 func dumpCpuinfo(ctx context.Context, w io.Writer) error { 88 info, err := cpu.InfoWithContext(ctx) 89 if err != nil { 90 return fmt.Errorf("error getting cpuinfo: %v", err) 91 } 92 s, err := json.MarshalIndent(info, "", " ") 93 if err != nil { 94 return fmt.Errorf("error marshaling cpuinfo: %v", err) 95 } 96 _, err = w.Write(s) 97 return err 98 } 99 100 func dumpLoadinfo(ctx context.Context, w io.Writer) error { 101 type loadinfo struct { 102 Avg *load.AvgStat `json:"average"` 103 Misc *load.MiscStat `json:"miscellaneous"` 104 } 105 var info loadinfo 106 avg, err := load.AvgWithContext(ctx) 107 if err != nil { 108 return fmt.Errorf("error getting load averages: %v", err) 109 } 110 info.Avg = avg 111 misc, err := load.MiscWithContext(ctx) 112 if err != nil { 113 return fmt.Errorf("error getting miscellaneous load stats: %v", err) 114 } 115 info.Misc = misc 116 s, err := json.MarshalIndent(info, "", " ") 117 if err != nil { 118 return fmt.Errorf("error marshaling loadinfo: %v", err) 119 } 120 _, err = w.Write(s) 121 return err 122 } 123 124 func dumpMeminfo(ctx context.Context, w io.Writer) error { 125 type meminfo struct { 126 Virtual *mem.VirtualMemoryStat `json:"virtualMemory"` 127 Runtime runtime.MemStats `json:"goRuntime"` 128 } 129 var info meminfo 130 vmem, err := mem.VirtualMemoryWithContext(ctx) 131 if err != nil { 132 return fmt.Errorf("error getting virtual memory stats: %v", err) 133 } 134 info.Virtual = vmem 135 runtime.ReadMemStats(&info.Runtime) 136 s, err := json.MarshalIndent(info, "", " ") 137 if err != nil { 138 return fmt.Errorf("error marshaling meminfo: %v", err) 139 } 140 _, err = w.Write(s) 141 if err != nil { 142 return fmt.Errorf("error writing memory stats: %v", err) 143 } 144 return nil 145 } 146 147 // dumpGoroutine writes current goroutines with human-readable source 148 // locations. 149 func dumpGoroutine(ctx context.Context, w io.Writer) error { 150 p := pprof.Lookup("goroutine") 151 if p == nil { 152 panic("no goroutine profile") 153 } 154 // debug == 2 prints goroutine stacks in the same form as that printed for 155 // an unrecovered panic. 156 return p.WriteTo(w, 2) 157 } 158 159 // dumpPprofHeap writes a pprof heap profile. 160 func dumpPprofHeap(ctx context.Context, w io.Writer) error { 161 p := pprof.Lookup("heap") 162 if p == nil { 163 panic("no heap profile") 164 } 165 return p.WriteTo(w, 0) 166 } 167 168 // dumpPprofMutex writes a fraction of the stack traces of goroutines with 169 // contended mutexes. 170 func dumpPprofMutex(ctx context.Context, w io.Writer) error { 171 p := pprof.Lookup("mutex") 172 if p == nil { 173 panic("no mutex profile") 174 } 175 // debug == 1 makes use function names instead of hexadecimal addresses, so 176 // it can also be human-readable. 177 return p.WriteTo(w, 1) 178 } 179 180 // dumpPprofHeap writes a pprof CPU profile sampled for 30 seconds or until the 181 // context is done, whichever is shorter. 182 func dumpPprofProfile(ctx context.Context, w io.Writer) error { 183 if err := pprof.StartCPUProfile(w); err != nil { 184 return err 185 } 186 startTime := time.Now() 187 defer pprof.StopCPUProfile() 188 select { 189 case <-time.After(30 * time.Second): 190 case <-ctx.Done(): 191 d := time.Since(startTime) 192 log.Debug.Printf("dump: CPU profile cut short to %s", d.String()) 193 } 194 return nil 195 } 196 197 // dumpVars writes public variables exported by the expvar package. The output 198 // is equivalent to the output of the "/debug/vars" endpoint. 199 func dumpVars(ctx context.Context, w io.Writer) error { 200 if _, err := fmt.Fprintf(w, "{\n"); err != nil { 201 return err 202 } 203 var ( 204 err error 205 first = true 206 ) 207 expvar.Do(func(kv expvar.KeyValue) { 208 if !first { 209 if _, err = fmt.Fprintf(w, ",\n"); err != nil { 210 return 211 } 212 } 213 first = false 214 if _, err = fmt.Fprintf(w, "%q: %s", kv.Key, kv.Value); err != nil { 215 return 216 } 217 }) 218 if err != nil { 219 return err 220 } 221 if _, err := fmt.Fprintf(w, "\n}\n"); err != nil { 222 return err 223 } 224 return nil 225 } 226 227 // Func is the type of a function that is registered in (*Registry).Register to