1 package server
2
3 import (
4 "bytes"
5 "flag"
6 "fmt"
7 "io"
8 "net/http"
9 hpprof "net/http/pprof"
10 "os"
11 "runtime/debug"
12 "runtime/pprof"
13 "strconv"
14 "strings"
15
16 "golang.conradwood.net/go-easyops/appinfo"
17 "golang.conradwood.net/go-easyops/auth"
18 "golang.conradwood.net/go-easyops/client"
19 "golang.conradwood.net/go-easyops/cmdline"
20 "golang.conradwood.net/go-easyops/common"
21 gcom "golang.conradwood.net/go-easyops/common"
22 pp "golang.conradwood.net/go-easyops/profiling"
23 "golang.conradwood.net/go-easyops/utils"
24 "google.golang.org/grpc"
25 )
26
27 func debugHandler(w http.ResponseWriter, req *http.Request) {
28 p := req.URL.Path
29 z := strings.Split(p, "/")
30 if len(z) == 0 {
31 fmt.Printf("Invalid debug request: %s\n", p)
32 return
33 }
34 lp := z[len(z)-1]
35 fmt.Printf("Last part: %s\n", lp)
36 if lp == "cpu" {
37 debugCpuHandler(w, req)
38 return
39
40 }
41
42 if lp == "heapdump" {
43 writeHeap(w, req)
44 return
45 }
46 if lp == "pprofheapdump" {
47 pprof_writeHeap(w, req)
48 return
49 }
50 if lp == "info" {
51 hpprof.Index(w, req)
52 return
53 }
54 if lp == "goroutine" {
55 profile := pprof.Lookup(lp)
56 if profile != nil {
57 serve_debug_profile(profile, w, req)
58 return
59 }
60 }
61 h := hpprof.Handler(lp)
62 if h == nil {
63 fmt.Printf("[go-easyops] no such handler:%s\n", lp)
64 return
65 }
66 h.ServeHTTP(w, req)
67
68 }
69 func serve_debug_profile(p *pprof.Profile, w http.ResponseWriter, req *http.Request) {
70 buf := &bytes.Buffer{}
71 p.WriteTo(buf, 1)
72 b := buf.Bytes()
73 b = bytes.ReplaceAll(b, []byte("\n"), []byte("<br/>"))
74 bold := []string{"golang.conradwood.net", "golang.singingcat.net", "golang.yacloud.eu"}
75 for _, bol := range bold {
76 b = bytes.ReplaceAll(b, []byte(bol), []byte("<b>"+bol+"</b>"))
77 }
78 w.Header()["Content-Type"] = []string{"text/html"}
79 w.Write([]byte("<html><body>"))
80 w.Write(b)
81 w.Write([]byte("</body></html>"))
82 }
83
84 func pprof_writeHeap(w http.ResponseWriter, req *http.Request) {
85 h := hpprof.Handler("heap")
86 h.ServeHTTP(w, req)
87 }
88
89 func writeHeap(w http.ResponseWriter, req *http.Request) {
90 filename := "dump"
91 f, err := os.Create(filename)
92 if err != nil {
93 http.Error(w, fmt.Sprintf("Failure opening file %s", err), 404)
94 return
95 }
96 debug.WriteHeapDump(f.Fd())
97 f.Close()
98
99 fd, err := os.Open(filename)
100 if err != nil {
101
102 http.Error(w, fmt.Sprintf("File not open:%s", err), 500)
103 return
104 }
105 defer fd.Close()
106
107
108 FileStat, _ := fd.Stat()
109 FileSize := strconv.FormatInt(FileStat.Size(), 10)
110
111
112 w.Header().Set("Content-Disposition", "attachment; filename="+filename)
113 w.Header().Set("Content-Type", "application/octet-stream")
114 w.Header().Set("Content-Length", FileSize)
115
116 io.Copy(w, fd)
117 return
118 }
119
120 func debugCpuHandler(w http.ResponseWriter, req *http.Request) {
121 err := req.ParseForm()
122 if err != nil {
123 w.WriteHeader(500)
124 fmt.Fprintf(w, "Failed to parse form %s", err)
125 return
126 }
127 if len(req.Form["Download"]) != 0 {
128 if pp.IsActive() {
129 w.WriteHeader(409)
130 fmt.Fprintf(w, "Download unavailable whilst profiling is active")
131 return
132 }
133 b := pp.GetBuf()
134 if b.Len() == 0 {
135 w.WriteHeader(404)
136 fmt.Fprintf(w, "No profiling data available. Enable profiling for a longer period of time perhaps?")
137 return
138 }
139 w.Header().Set("Content-Disposition", "attachment; filename=cpuprofile")
140 w.Header().Set("Content-Type", "application/octet-stream")
141 w.Header().Set("Content-Length", fmt.Sprintf("%d", b.Len()))
142 w.Write(b.Bytes())
143 return
144 }
145 fmt.Fprintf(w, ("<html><body>"))
146 if len(req.Form["toggle"]) != 0 {
147 fmt.Fprintf(w, "toggled<br/>")
148 pp.Toggle()
149 }
150 s := "Inactive"
151 if pp.IsActive() {
152 s = "Active"
153 }
154 fmt.Fprintf(w, "CPU Profiling: %s\n", s)
155 fmt.Fprintf(w, " <a href=\"?toggle\">Toggle</a></br>")
156 if pp.IsActive() {
157 fmt.Fprintf(w, "Download unavailable whilst profiling is active<br/>")
158 } else {
159 b := pp.GetBuf()
160 if b.Len() == 0 {
161 fmt.Fprintf(w, "Download unavailable - enable profiling first<br/>")
162 } else {
163 fmt.Fprintf(w, "<a href=\"?Download\">Download</a> most recent profile</br>")
164 }
165 }
166 fmt.Fprintf(w, "</body></html>")
167 return
168 }
169 func helpHandler(w http.ResponseWriter, req *http.Request, sd *serverDef) {
170 s := `<html><body>
171 <a href="/internal/pleaseshutdown">shutdown</a><br/>
172 <a href="/internal/health">server health</a><br/>
173 <a href="/internal/service-info/version">VersionInfo</a><br/>
174 <a href="/internal/service-info/metrics">metrics</a><br/>
175 <a href="/internal/clearcache">clearcache</a> (append /name to clear a specific cache)<br/>
176 <a href="/internal/parameters">parameters</a><br/>
177 <a href="/internal/service-info/infoproviders">Registered Info providers</a><br/>
178 <a href="/internal/service-info/grpc-connections">GRPC Connections</a><br/>
179 <a href="/internal/service-info/grpc-callers">GRPC Server Caller list</a> (who called this service)<br/>
180 <a href="/internal/service-info/dependencies">Registered GRPC Dependencies</a><br/>
181 <a href="/internal/debug/info">Go-Profiler</a><br/>
182 <a href="/internal/debug/cpu">CPU Profiler</a><br/>
183 <a href="/internal/debug/heapdump">Download Debug Heap Dump</a> as generated by debug.WriteHeapDump. See format description <a href="https://go.dev/wiki/heapdump15-through-heapdump17">here</a><br/>
184 <a href="/internal/debug/pprofheapdump">Download PProf Heap Dump</a> as generated by pprof.Handler("heap"). useful with, for example, "go tool pprof -http=:8082 /tmp/pprofdump.bin"<br/>
185
186 </body></html>
187 `
188 fmt.Fprintf(w, "%s", s)
189 }
190
191 func healthzHandler(w http.ResponseWriter, req *http.Request, sd *serverDef) {
192 fmt.Fprintf(w, "%s", getHealthString())
193 }
194
195
196 func serveServiceInfo(w http.ResponseWriter, req *http.Request, sd *serverDef) {
197 p := req.URL.Path
198 if strings.HasPrefix(p, "/internal/service-info/name") {
199 fmt.Fprint(w, (sd.name))
200 } else if strings.HasPrefix(p, "/internal/service-info/version") {
201 serveVersion(w, req, sd)
202 } else if strings.HasPrefix(p, "/internal/service-info/grpc-connections") {
203 serveGRPCConnections(w, req, sd)
204 } else if strings.HasPrefix(p, "/internal/service-info/grpc-callers") {
205 serveGRPCCallers(w, req, sd)
206 } else if strings.HasPrefix(p, "/internal/service-info/dependencies") {
207 serveDependencies(w, req, sd)
208 } else if strings.HasPrefix(p, "/internal/service-info/infoproviders") {
209 serveInfo(w, req, sd)
210 } else if strings.HasPrefix(p, "/internal/service-info/metrics") {
211 fmt.Printf("Request path: \"%s\"\n", p)
212 m := strings.TrimPrefix(p, "/internal/service-info/metrics")
213 m = strings.TrimLeft(m, "/")
214 } else {
215 fmt.Printf("Invalid path: \"%s\"\n", p)
216 }
217 }
218
219
220 func serveInfo(w http.ResponseWriter, req *http.Request, sd *serverDef) {
221 w.Header().Set("content-type", "text/plain")
222 ms := gcom.GetText()
223 sb := strings.Builder{}
224
225 for v, s := range ms {
226 sb.WriteString("-------------------- " + v + "\n")
227 sb.WriteString(s + "\n")
228 }
229 fmt.Fprint(w, sb.String())
230
231 }
232
233
234 func serveGRPCCallers(w http.ResponseWriter, req *http.Request, sd *serverDef) {
235 usage_info := GetUsageInfo()
236 ct_service := 0
237 ct_methods := 0
238 ct_callers := 0
239 for _, service := range usage_info.Services() {
240 ct_service++
241 for _, method := range service.Methods() {
242 ct_methods++
243 ct_callers = ct_callers + len(method.Callers())
244
245 }
246 }
247 path := strings.TrimSuffix(req.URL.Path, "/")
248 fmt.Printf("[go-easyops] Path: \"%s\"\n", path)
249 if strings.HasSuffix(path, "/text") {
250 w.Header().Set("content-type", "text/plain")
251 fmt.Fprintf(w, "COUNT: services=%d, methods=%d, callers=%d\n", ct_service, ct_methods, ct_callers)
252 for _, service := range usage_info.Services() {
253 for _, method := range service.Methods() {
254 for _, callers := range method.Callers() {
255 fmt.Fprintf(w, "ENTRY: %s.%s %s\n", service.Name(), method.Name(), callers.String())
256 }
257 }
258 }
259 } else {
260 w.Header().Set("content-type", "text/html")
261 sb := strings.Builder{}
262 sb.WriteString(`<html><head><link rel="stylesheet" type="text/css" href="https://www.conradwood.net/stylesheet.css"/><title>GRPC Services Info</title><link rel="icon" type="image/ico" href="https://www.yacloud.eu/favicon.ico"/></head><body class="root">`)
263 sb.WriteString(`<section class="white">`)
264 sb.WriteString("<a href=\"grpc-callers/text/\">Text version</a>\n")
265 sb.WriteString(fmt.Sprintf("COUNT: services=%d, methods=%d, callers=%d\n", ct_service, ct_methods, ct_callers))
266 for _, service := range usage_info.Services() {
267 sb.WriteString(fmt.Sprintf("<h1>Service: %s</h1>\n", service.Name()))
268 for _, method := range service.Methods() {
269 sb.WriteString(fmt.Sprintf("<p>Method: <code>%s</code></p>\n", method.Name()))
270 sb.WriteString("<ul>")
271 for _, caller := range method.Callers() {
272 sb.WriteString("<li>")
273 usages := caller.Usages()
274 user_s := auth.UserIDString(caller.User()) + " " + caller.User().Email
275 last_call := utils.TimeString(caller.LastCallTime())
276 errs := caller.Errors()
277 rate := caller.ErrorRate()
278 sb.WriteString(fmt.Sprintf("called %d times by: %s (last at %s), failures %d, failure-rate: %0.1f", usages, user_s, last_call, errs, rate))
279 sb.WriteString(`%%`)
280 sb.WriteString("\n")
281 sb.WriteString("</li>")
282 }
283 sb.WriteString("</ul>")
284 sb.WriteString(`<hr/>`)
285 }
286 }
287 sb.WriteString(`</section></body></html>`)
288 x := sb.String()
289 x = strings.ReplaceAll(x, "\n", "<br/>\n")
290 fmt.Fprint(w, x)
291 }
292 }
293
294
295 func serveDependencies(w http.ResponseWriter, req *http.Request, sd *serverDef) {
296 s := client.GetDependencies()
297 fmt.Fprintf(w, "# %d registered dependencies\n", len(s))
298 for _, r := range s {
299 fmt.Fprintf(w, "%s\n", r)
300 }
301 }
302
303
304 func serveGRPCConnections(w http.ResponseWriter, req *http.Request, sd *serverDef) {
305 s := common.GetConnectionNames()
306 fmt.Fprintf(w, "# %d requested connections\n", len(s))
307 for _, r := range s {
308 fmt.Fprintf(w, "%s\n", r.Name)
309 }
310 bs := common.GetBlockedConnectionNames()
311 fmt.Fprintf(w, "# %d blocked connections\n", len(bs))
312 for _, r := range bs {
313 fmt.Fprintf(w, "%s\n", r.Name)
314 }
315 }
316
317
318 func serveVersion(w http.ResponseWriter, req *http.Request, sd *serverDef) {
319 fmt.Fprintf(w, "go_framework_buildid: %d\n", cmdline.BUILD_NUMBER)
320 fmt.Fprintf(w, "go_framework_timestamp: %d\n", cmdline.BUILD_TIMESTAMP)
321 fmt.Fprintf(w, "go_framework_description: %s\n", cmdline.BUILD_DESCRIPTION)
322 fmt.Fprintf(w, "app_buildid: %d\n", appinfo.AppInfo().Number)
323 fmt.Fprintf(w, "app_timestamp: %d\n", appinfo.AppInfo().Timestamp)
324 fmt.Fprintf(w, "app_description: %s\n", appinfo.AppInfo().Description)
325 fmt.Fprintf(w, "app_repository: %s\n", appinfo.AppInfo().RepositoryName)
326 fmt.Fprintf(w, "app_repository_id: %d\n", appinfo.AppInfo().RepositoryID)
327 fmt.Fprintf(w, "app_artefact_id: %d\n", appinfo.AppInfo().ArtefactID)
328 fmt.Fprintf(w, "app_commit: %s\n", appinfo.AppInfo().CommitID)
329
330 }
331
332
333 func paraHandler(w http.ResponseWriter, req *http.Request, sd *serverDef) {
334 errno := 402
335 err := req.ParseForm()
336 if err != nil {
337 fmt.Fprintf(w, "Failed to parse request: %s\n", err)
338 return
339 }
340 if len(req.Form) == 0 {
341 flag.VisitAll(func(f *flag.Flag) {
342 s := "SET"
343 if fmt.Sprintf("%v", f.Value) == fmt.Sprintf("%v", f.DefValue) {
344 s = "DEFAULT"
345 }
346 fmt.Fprintf(w, "%s %s %s %s\n", "STRING", s, f.Name, f.Value)
347 })
348 return
349 }
350 for name, value := range req.Form {
351 if len(value) != 1 {
352 http.Error(w, fmt.Sprintf("odd number of values for %s: %d (expected 1)\n", name, len(value)), errno)
353 return
354 }
355
356 f := flag.Lookup(name)
357 if f == nil {
358 http.Error(w, "No such flag\n", errno)
359 return
360 }
361 err = f.Value.Set(value[0])
362 if err != nil {
363 http.Error(w, fmt.Sprintf("Cannot set value of %s to %s: %s\n", name, value, err), errno)
364 return
365 }
366 err = ipc_send_new_para(sd, name, value[0])
367 if err != nil {
368 fmt.Printf("[go-easyops] failed to send parameter change via ipc (%s=%s): %s\n", name, value[0], err)
369
370 }
371
372 }
373 pp.ProfilingCheckStart()
374 fmt.Fprintf(w, "Done")
375 }
376
377
378 func pleaseShutdown(w http.ResponseWriter, req *http.Request, s *grpc.Server) {
379 stopping(make(chan bool, 10))
380 fmt.Fprintf(w, "OK\n")
381 fmt.Printf("Received request to shutdown.\n")
382 s.Stop()
383 os.Exit(0)
384 }
385
View as plain text