Last active
October 3, 2024 14:08
-
-
Save enricofoltran/10b4a980cd07cb02836f70a4ab3e72d7 to your computer and use it in GitHub Desktop.
A simple golang web server with basic logging, tracing, health check, graceful shutdown and zero dependencies
This file contains 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
package main | |
import ( | |
"context" | |
"flag" | |
"fmt" | |
"log" | |
"net/http" | |
"os" | |
"os/signal" | |
"sync/atomic" | |
"time" | |
) | |
type key int | |
const ( | |
requestIDKey key = 0 | |
) | |
var ( | |
listenAddr string | |
healthy int32 | |
) | |
func main() { | |
flag.StringVar(&listenAddr, "listen-addr", ":5000", "server listen address") | |
flag.Parse() | |
logger := log.New(os.Stdout, "http: ", log.LstdFlags) | |
logger.Println("Server is starting...") | |
router := http.NewServeMux() | |
router.Handle("/", index()) | |
router.Handle("/healthz", healthz()) | |
nextRequestID := func() string { | |
return fmt.Sprintf("%d", time.Now().UnixNano()) | |
} | |
server := &http.Server{ | |
Addr: listenAddr, | |
Handler: tracing(nextRequestID)(logging(logger)(router)), | |
ErrorLog: logger, | |
ReadTimeout: 5 * time.Second, | |
WriteTimeout: 10 * time.Second, | |
IdleTimeout: 15 * time.Second, | |
} | |
done := make(chan bool) | |
quit := make(chan os.Signal, 1) | |
signal.Notify(quit, os.Interrupt) | |
go func() { | |
<-quit | |
logger.Println("Server is shutting down...") | |
atomic.StoreInt32(&healthy, 0) | |
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) | |
defer cancel() | |
server.SetKeepAlivesEnabled(false) | |
if err := server.Shutdown(ctx); err != nil { | |
logger.Fatalf("Could not gracefully shutdown the server: %v\n", err) | |
} | |
close(done) | |
}() | |
logger.Println("Server is ready to handle requests at", listenAddr) | |
atomic.StoreInt32(&healthy, 1) | |
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { | |
logger.Fatalf("Could not listen on %s: %v\n", listenAddr, err) | |
} | |
<-done | |
logger.Println("Server stopped") | |
} | |
func index() http.Handler { | |
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
if r.URL.Path != "/" { | |
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) | |
return | |
} | |
w.Header().Set("Content-Type", "text/plain; charset=utf-8") | |
w.Header().Set("X-Content-Type-Options", "nosniff") | |
w.WriteHeader(http.StatusOK) | |
fmt.Fprintln(w, "Hello, World!") | |
}) | |
} | |
func healthz() http.Handler { | |
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
if atomic.LoadInt32(&healthy) == 1 { | |
w.WriteHeader(http.StatusNoContent) | |
return | |
} | |
w.WriteHeader(http.StatusServiceUnavailable) | |
}) | |
} | |
func logging(logger *log.Logger) func(http.Handler) http.Handler { | |
return func(next http.Handler) http.Handler { | |
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
defer func() { | |
requestID, ok := r.Context().Value(requestIDKey).(string) | |
if !ok { | |
requestID = "unknown" | |
} | |
logger.Println(requestID, r.Method, r.URL.Path, r.RemoteAddr, r.UserAgent()) | |
}() | |
next.ServeHTTP(w, r) | |
}) | |
} | |
} | |
func tracing(nextRequestID func() string) func(http.Handler) http.Handler { | |
return func(next http.Handler) http.Handler { | |
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
requestID := r.Header.Get("X-Request-Id") | |
if requestID == "" { | |
requestID = nextRequestID() | |
} | |
ctx := context.WithValue(r.Context(), requestIDKey, requestID) | |
w.Header().Set("X-Request-Id", requestID) | |
next.ServeHTTP(w, r.WithContext(ctx)) | |
}) | |
} | |
} |
I added a -d option to specify a directory to serve out, but im no good with forking and pull requests. I think I might have removed some useful functionality in the process :)) but if you like that flag, you can find the repo pinned to my github and hack away at it
This is cash-money awesome! Thank you for sharing this!
Any pointers on how to include the http.statuscode in logs, please?
Thanks for sharing
I'd like to use this code in an open source static site generator. It's MIT licensed. Can I use this code, or at least a version of it? And if so, what's your license for it? Thanks!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
If you are interested in the GET query parameters, check the other fields and methods of URL, such as
RequestURI
,RawQuery
or even just callingString()
onURL
itself.If you also want the POST variables, you need to check out Request.Form and Request.ParseForm(). Note it's usually a bad idea to log POST request variables as they might be rather large or contain sensitive information such as passwords.