github.com/safing/portbase@v0.19.5/api/endpoints_debug.go (about) 1 package api 2 3 import ( 4 "bytes" 5 "context" 6 "errors" 7 "fmt" 8 "net/http" 9 "os" 10 "runtime/pprof" 11 "strings" 12 "time" 13 14 "github.com/safing/portbase/info" 15 "github.com/safing/portbase/modules" 16 "github.com/safing/portbase/utils/debug" 17 ) 18 19 func registerDebugEndpoints() error { 20 if err := RegisterEndpoint(Endpoint{ 21 Path: "ping", 22 Read: PermitAnyone, 23 ActionFunc: ping, 24 Name: "Ping", 25 Description: "Pong.", 26 }); err != nil { 27 return err 28 } 29 30 if err := RegisterEndpoint(Endpoint{ 31 Path: "ready", 32 Read: PermitAnyone, 33 ActionFunc: ready, 34 Name: "Ready", 35 Description: "Check if Portmaster has completed starting and is ready.", 36 }); err != nil { 37 return err 38 } 39 40 if err := RegisterEndpoint(Endpoint{ 41 Path: "debug/stack", 42 Read: PermitAnyone, 43 DataFunc: getStack, 44 Name: "Get Goroutine Stack", 45 Description: "Returns the current goroutine stack.", 46 }); err != nil { 47 return err 48 } 49 50 if err := RegisterEndpoint(Endpoint{ 51 Path: "debug/stack/print", 52 Read: PermitAnyone, 53 ActionFunc: printStack, 54 Name: "Print Goroutine Stack", 55 Description: "Prints the current goroutine stack to stdout.", 56 }); err != nil { 57 return err 58 } 59 60 if err := RegisterEndpoint(Endpoint{ 61 Path: "debug/cpu", 62 MimeType: "application/octet-stream", 63 Read: PermitAnyone, 64 DataFunc: handleCPUProfile, 65 Name: "Get CPU Profile", 66 Description: strings.ReplaceAll(`Gather and return the CPU profile. 67 This data needs to gathered over a period of time, which is specified using the duration parameter. 68 69 You can easily view this data in your browser with this command (with Go installed): 70 "go tool pprof -http :8888 http://127.0.0.1:817/api/v1/debug/cpu" 71 `, `"`, "`"), 72 Parameters: []Parameter{{ 73 Method: http.MethodGet, 74 Field: "duration", 75 Value: "10s", 76 Description: "Specify the formatting style. The default is simple markdown formatting.", 77 }}, 78 }); err != nil { 79 return err 80 } 81 82 if err := RegisterEndpoint(Endpoint{ 83 Path: "debug/heap", 84 MimeType: "application/octet-stream", 85 Read: PermitAnyone, 86 DataFunc: handleHeapProfile, 87 Name: "Get Heap Profile", 88 Description: strings.ReplaceAll(`Gather and return the heap memory profile. 89 90 You can easily view this data in your browser with this command (with Go installed): 91 "go tool pprof -http :8888 http://127.0.0.1:817/api/v1/debug/heap" 92 `, `"`, "`"), 93 }); err != nil { 94 return err 95 } 96 97 if err := RegisterEndpoint(Endpoint{ 98 Path: "debug/allocs", 99 MimeType: "application/octet-stream", 100 Read: PermitAnyone, 101 DataFunc: handleAllocsProfile, 102 Name: "Get Allocs Profile", 103 Description: strings.ReplaceAll(`Gather and return the memory allocation profile. 104 105 You can easily view this data in your browser with this command (with Go installed): 106 "go tool pprof -http :8888 http://127.0.0.1:817/api/v1/debug/allocs" 107 `, `"`, "`"), 108 }); err != nil { 109 return err 110 } 111 112 if err := RegisterEndpoint(Endpoint{ 113 Path: "debug/info", 114 Read: PermitAnyone, 115 DataFunc: debugInfo, 116 Name: "Get Debug Information", 117 Description: "Returns debugging information, including the version and platform info, errors, logs and the current goroutine stack.", 118 Parameters: []Parameter{{ 119 Method: http.MethodGet, 120 Field: "style", 121 Value: "github", 122 Description: "Specify the formatting style. The default is simple markdown formatting.", 123 }}, 124 }); err != nil { 125 return err 126 } 127 128 return nil 129 } 130 131 // ping responds with pong. 132 func ping(ar *Request) (msg string, err error) { 133 // TODO: Remove upgrade to "ready" when all UI components have transitioned. 134 if modules.IsStarting() || modules.IsShuttingDown() { 135 return "", ErrorWithStatus(errors.New("portmaster is not ready, reload (F5) to try again"), http.StatusTooEarly) 136 } 137 138 return "Pong.", nil 139 } 140 141 // ready checks if Portmaster has completed starting. 142 func ready(ar *Request) (msg string, err error) { 143 if modules.IsStarting() || modules.IsShuttingDown() { 144 return "", ErrorWithStatus(errors.New("portmaster is not ready, reload (F5) to try again"), http.StatusTooEarly) 145 } 146 return "Portmaster is ready.", nil 147 } 148 149 // getStack returns the current goroutine stack. 150 func getStack(_ *Request) (data []byte, err error) { 151 buf := &bytes.Buffer{} 152 err = pprof.Lookup("goroutine").WriteTo(buf, 1) 153 if err != nil { 154 return nil, err 155 } 156 return buf.Bytes(), nil 157 } 158 159 // printStack prints the current goroutine stack to stderr. 160 func printStack(_ *Request) (msg string, err error) { 161 _, err = fmt.Fprint(os.Stderr, "===== PRINTING STACK =====\n") 162 if err == nil { 163 err = pprof.Lookup("goroutine").WriteTo(os.Stderr, 1) 164 } 165 if err == nil { 166 _, err = fmt.Fprint(os.Stderr, "===== END OF STACK =====\n") 167 } 168 if err != nil { 169 return "", err 170 } 171 return "stack printed to stdout", nil 172 } 173 174 // handleCPUProfile returns the CPU profile. 175 func handleCPUProfile(ar *Request) (data []byte, err error) { 176 // Parse duration. 177 duration := 10 * time.Second 178 if durationOption := ar.Request.URL.Query().Get("duration"); durationOption != "" { 179 parsedDuration, err := time.ParseDuration(durationOption) 180 if err != nil { 181 return nil, fmt.Errorf("failed to parse duration: %w", err) 182 } 183 duration = parsedDuration 184 } 185 186 // Indicate download and filename. 187 ar.ResponseHeader.Set( 188 "Content-Disposition", 189 fmt.Sprintf(`attachment; filename="portmaster-cpu-profile_v%s.pprof"`, info.Version()), 190 ) 191 192 // Start CPU profiling. 193 buf := new(bytes.Buffer) 194 if err := pprof.StartCPUProfile(buf); err != nil { 195 return nil, fmt.Errorf("failed to start cpu profile: %w", err) 196 } 197 198 // Wait for the specified duration. 199 select { 200 case <-time.After(duration): 201 case <-ar.Context().Done(): 202 pprof.StopCPUProfile() 203 return nil, context.Canceled 204 } 205 206 // Stop CPU profiling and return data. 207 pprof.StopCPUProfile() 208 return buf.Bytes(), nil 209 } 210 211 // handleHeapProfile returns the Heap profile. 212 func handleHeapProfile(ar *Request) (data []byte, err error) { 213 // Indicate download and filename. 214 ar.ResponseHeader.Set( 215 "Content-Disposition", 216 fmt.Sprintf(`attachment; filename="portmaster-memory-heap-profile_v%s.pprof"`, info.Version()), 217 ) 218 219 buf := new(bytes.Buffer) 220 if err := pprof.Lookup("heap").WriteTo(buf, 0); err != nil { 221 return nil, fmt.Errorf("failed to write heap profile: %w", err) 222 } 223 return buf.Bytes(), nil 224 } 225 226 // handleAllocsProfile returns the Allocs profile. 227 func handleAllocsProfile(ar *Request) (data []byte, err error) { 228 // Indicate download and filename. 229 ar.ResponseHeader.Set( 230 "Content-Disposition", 231 fmt.Sprintf(`attachment; filename="portmaster-memory-allocs-profile_v%s.pprof"`, info.Version()), 232 ) 233 234 buf := new(bytes.Buffer) 235 if err := pprof.Lookup("allocs").WriteTo(buf, 0); err != nil { 236 return nil, fmt.Errorf("failed to write allocs profile: %w", err) 237 } 238 return buf.Bytes(), nil 239 } 240 241 // debugInfo returns the debugging information for support requests. 242 func debugInfo(ar *Request) (data []byte, err error) { 243 // Create debug information helper. 244 di := new(debug.Info) 245 di.Style = ar.Request.URL.Query().Get("style") 246 247 // Add debug information. 248 di.AddVersionInfo() 249 di.AddPlatformInfo(ar.Context()) 250 di.AddLastReportedModuleError() 251 di.AddLastUnexpectedLogs() 252 di.AddGoroutineStack() 253 254 // Return data. 255 return di.Bytes(), nil 256 }