Skip to content

Instantly share code, notes, and snippets.

@josue
Last active August 4, 2023 21:23
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save josue/7c70d7b1f3d0e374a465a0718ac82aff to your computer and use it in GitHub Desktop.
Save josue/7c70d7b1f3d0e374a465a0718ac82aff to your computer and use it in GitHub Desktop.
Simple Golang HTTP server with signal capturing (ie: SIGINT/SIGTERM) & pprof debugger
/*
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