...

Source file src/golang.conradwood.net/go-easyops/server/http-handler.go

Documentation: golang.conradwood.net/go-easyops/server

     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" { // tested, works
    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  	//todo
    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  	//Check if file exists and open
    99  	fd, err := os.Open(filename)
   100  	if err != nil {
   101  		//File not found, send 404
   102  		http.Error(w, fmt.Sprintf("File not open:%s", err), 500)
   103  		return
   104  	}
   105  	defer fd.Close() //Close after function return
   106  
   107  	//Get the file size
   108  	FileStat, _ := fd.Stat()                           //Get info from file
   109  	FileSize := strconv.FormatInt(FileStat.Size(), 10) //Get file size as a string
   110  
   111  	//Send the headers
   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) //'Copy' the file to the client
   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, "&nbsp<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  // this services the /service-info/ url
   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  // serve /internal/service-info/grpc-callers
   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  // serve /internal/service-info/grpc-callers
   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  // serve /internal/service-info/dependencies
   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  // serve /internal/service-info/grpc-connections
   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  // services the version url /internal/version/go-framework
   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  // this servers /internal/parameters url
   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  		//fmt.Fprintf(w, "Setting %s to %s\n", name, value)
   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  			// no further action, considering this somewhat optional for now
   370  		}
   371  
   372  	}
   373  	pp.ProfilingCheckStart() // make it pick up on changes to flag if any
   374  	fmt.Fprintf(w, "Done")
   375  }
   376  
   377  // this services the /pleaseshutdown url
   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()   // maybe even s.GracefulStop() ?
   383  	os.Exit(0) // i'd prefer not to exit here unless something is relying on it.
   384  }
   385  

View as plain text