Last active
August 4, 2023 21:23
-
-
Save josue/7c70d7b1f3d0e374a465a0718ac82aff to your computer and use it in GitHub Desktop.
Simple Golang HTTP server with signal capturing (ie: SIGINT/SIGTERM) & pprof debugger
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
go build for linux: | |
env GOOS=linux go build -o /tmp/server server.go | |
run with docker: | |
docker run -it --rm --name test -v /tmp/server:/server -p 3000:80 -p 3001:6060 alpine /server | |
run pprof debugger: | |
go get github.com/google/pprof | |
pprof --seconds 30 -http=:4444 /tmp/server http://localhost:3001/debug/pprof/profile | |
benchmarking with wrk: | |
brew install wrk | |
wrk -d 5s http://localhost:3000/ | |
*/ | |
package main | |
import ( | |
"encoding/json" | |
"fmt" | |
"log" | |
"net/http" | |
"os" | |
"os/signal" | |
"syscall" | |
"time" | |
_ "net/http/pprof" | |
) | |
// http routes | |
var routes = map[string]string{ | |
"/": "apiRoot", | |
"/healthz": "apiHealthz", | |
"/error": "apiError", | |
} | |
// handle route paths with methods | |
func routeHandler(w http.ResponseWriter, r *http.Request) error { | |
route, ok := routes[r.RequestURI] | |
if !ok { | |
return fmt.Errorf("Route %s path does not exist", r.RequestURI) | |
} | |
switch route { | |
case "apiRoot": | |
return apiRoot(w, r) | |
case "apiHealthz": | |
return apiHealthz(w, r) | |
case "apiError": | |
return apiError(w, r) | |
default: | |
return fmt.Errorf("Route %s method not exist", r.RequestURI) | |
} | |
} | |
// route methods | |
// root/index path | |
func apiRoot(w http.ResponseWriter, r *http.Request) error { | |
data := map[string]string{"hello": "welcome"} | |
return outputJSON(w, data) | |
} | |
// simple health check | |
func apiHealthz(w http.ResponseWriter, r *http.Request) error { | |
data := map[string]interface{}{"alive": time.Now().Unix()} | |
return outputJSON(w, data) | |
} | |
// simulate error with 1 second delay | |
func apiError(w http.ResponseWriter, r *http.Request) error { | |
defer timeTrack(time.Now(), "/error endpoint bug taking too long") | |
time.Sleep(1 * time.Second) | |
return fmt.Errorf("some error") | |
} | |
// capture errors | |
func errorHandler(f func(http.ResponseWriter, *http.Request) error) http.HandlerFunc { | |
return func(w http.ResponseWriter, r *http.Request) { | |
startTime := time.Now() | |
w.Header().Set("Content-Type", "application/json") | |
err := f(w, r) | |
if err != nil { | |
errMsg := fmt.Sprintf("{ \"error:\": \"%s\" }", err.Error()) | |
http.Error(w, errMsg, http.StatusInternalServerError) | |
log.Printf("[latency: %s] %d Error - Path: %s -- err: %v", time.Since(startTime), http.StatusInternalServerError, r.RequestURI, err) | |
return | |
} | |
log.Printf("[latency: %s] %d OK - Path: %s", time.Since(startTime), http.StatusOK, r.RequestURI) | |
} | |
} | |
// map to json string | |
func outputJSON(w http.ResponseWriter, data interface{}) error { | |
jsonString, err := json.Marshal(data) | |
if err != nil { | |
log.Fatalf("[Internal Error] Unable to stringify map argument, err: %v", err) | |
return fmt.Errorf("[Internal Error] JSON data") | |
} | |
fmt.Fprint(w, string(jsonString)) | |
return nil | |
} | |
// time track | |
func timeTrack(start time.Time, name string) { | |
elapsed := time.Since(start) | |
log.Printf("TimeTrack (%s) -> %s", elapsed, name) | |
} | |
// signal handler | |
func signalHandler(signal os.Signal) { | |
fmt.Printf("\nCaught signal: %+v", signal) | |
fmt.Println("\nWait for 1 second to finish processing") | |
time.Sleep(1 * time.Second) | |
switch signal { | |
case syscall.SIGHUP: // kill -SIGHUP XXXX | |
fmt.Println("- got hungup") | |
case syscall.SIGINT: // kill -SIGINT XXXX or Ctrl+c | |
fmt.Println("- got ctrl+c") | |
case syscall.SIGTERM: // kill -SIGTERM XXXX | |
fmt.Println("- got force stop") | |
case syscall.SIGQUIT: // kill -SIGQUIT XXXX | |
fmt.Println("- stop and core dump") | |
default: | |
fmt.Println("- unknown signal") | |
} | |
fmt.Println("\nFinished server cleanup") | |
os.Exit(0) | |
} | |
// initialize signal handler | |
func initSignals() { | |
var captureSignal = make(chan os.Signal, 1) | |
signal.Notify(captureSignal, syscall.SIGINT, syscall.SIGTERM, syscall.SIGABRT) | |
signalHandler(<-captureSignal) | |
} | |
// initialize PPROF debugger | |
func initPprofDebugger() { | |
port := "6060" // default port | |
if os.Getenv("PPROF_PORT") != "" { | |
port = os.Getenv("PPROF_PORT") | |
} | |
log.Println("Running pprof debugger on :" + port) | |
log.Println(fmt.Sprint(http.ListenAndServe(":"+port, nil).Error())) | |
} | |
// initialize server listener | |
func initApiServer() { | |
// initialize routes | |
for route, _ := range routes { | |
http.HandleFunc(route, errorHandler(routeHandler)) | |
} | |
port := "80" // default port | |
if os.Getenv("SERVER_PORT") != "" { | |
port = os.Getenv("SERVER_PORT") | |
} | |
log.Println("Running api server on port :" + port) | |
err := http.ListenAndServe(":"+port, nil) | |
if err != nil { | |
log.Printf("[Error] ListenAndServe: %v", err) | |
} | |
} | |
func main() { | |
// start PPROF debugger | |
go initPprofDebugger() | |
// capture signals | |
go initSignals() | |
// start HTTP server | |
initApiServer() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment