-
-
Save enricofoltran/10b4a980cd07cb02836f70a4ab3e72d7 to your computer and use it in GitHub Desktop.
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)) | |
}) | |
} | |
} |
Good work! If you are interested, here is a simplified version with the same features (and more): https://gist.github.com/creack/4c00ee404f2d7bd5983382cc93af5147
It’s usually a good idea to log incoming requests separately and before hand outgoing responses, for security audit and debugging reasons.
If you want to add simple level based logging (also to file) you can plug in
https://github.com/munirehmad/golanglog
Small race condition: the server shutdown might not be clean as the program exits as soon shutdown is called:
When Shutdown is called, Serve, ListenAndServe, and ListenAndServeTLS immediately return ErrServerClosed. Make sure the program doesn't exit and waits instead for Shutdown to return.
@elmiko thanks!
Hi @RomanMinkin, thanks! I've built this trying to avoid external dependencies and Go doesn't have a uuid package in the standard lib; that said, for a production grade app I probably implement a more robust nextRequestID
function with sometingh like uuid or https://github.com/sony/sonyflake
@creack thanks for sharing your variant, I like the way you used uptime to store server state!
@munirehmad In this small example I'm trying to avoid third party dependencies, but thanks for your suggestion.
@djui I think my program wait for server.Shutdown()
to return, correct me if I'm wrong.
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(), 5*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 != http.ErrServerClosed {
logger.Fatalf("Could not listen on %s: %v\n", listenAddr, err)
}
<-done
This will block until shutdown returns
@enricofoltran What @vic3lord and @djui are pointing to is that the Shutdown
call should finished before the program exits.
There is nothing set up to synchronize the goroutine with the singal trap which calls shutdown, with the exit of the program. Using a channel like above would work perfectly. Shutdown
will wait for inflight requests to be cleaned up, but will immediately unblock the main goroutine and the program will exit before the cleanup or its timeout happens.
Also https://gist.github.com/enricofoltran/10b4a980cd07cb02836f70a4ab3e72d7#file-main-go-L69
you will want to check for err != nil
as well. Otherwise, a nil error here would also result in a non-zero exit code (because of log.Fatalf). Though I don't know under which circumstances ListenAndServe
returns a nil error, instead of blocking indefinitely. But to rely on that being the case would be to rely on that behavior never changing.
Great to see how little code it takes to do something as sophisticated as this. Testament to how great Go is 👍
@djui @vic3lord @GeorgeMac after some testing I proved myself that you guys are right, thank you very much for pointing that out! I've updated the code
code organization:
https://gist.github.com/dstroot/faef20a6c863b5fa8e3438f6055e36cd
Alternate implementation for consideration https://github.com/moexmen/gokit/blob/master/web/server.go
I've had a problem with os.Interrupt
(syscall.SIGINT) not being enough depending on deploy method (fine for development). syscall.SIGTERM
(not on all systems) on linux/bsd seems to solve this when the process is detached from your user.
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
You can also listen for os.Kill
(https://golang.org/pkg/os/#Signal)
I started a repo based on this: https://github.com/Xeoncross/vanilla-go-server
Given the multiple revisions and 78 forks, would you be willing to put this into a full git repo, so that it's clearer/easier to see the latest version and also have separate Issue discussions about potential changes/improvements?
@xeoncross can you open a new issue in this repository? I'd like to better understand the problem and find a solution, thank you!
what if i want to make the server support both http and https?
Here's a git-push
to deploy version of this with HTTPS domain: https://hasura.io/hub/projects/shahidhk/simple-go-server
In the tracing middleware, shouldn't you get the Request ID from the Context, rather than from the request Header.
Please, correct me if I am wrong!
It turns out, maybe a simple server isn't quite so simple, compared to the typical "Hello World."
Anybody want to discuss the use of goroutines and other techniques in this code? We're usually told that goroutines aren't necessary because this is already done in the standard library, for example.
It seems clear that this relates to resiliency and so on, but what caused the authors (original and contributing) to arrive at these conclusions and include additional checks and handling code, at the cost of increased complexity? There would appear to be context--likely based on knowledge and direct experience--that is somewhat lost in the example.
A breakdown of the code and decisions behind its parts would likely be very educational for those among us who wouldn't know to include these things by default.
if i started this server from the commandline like go run server.go, how do you stop it?
It turns out, maybe a simple server isn't quite so simple, compared to the typical "Hello World."
Anybody want to discuss the use of goroutines and other techniques in this code? We're usually told that goroutines aren't necessary because this is already done in the standard library, for example.
It seems clear that this relates to resiliency and so on, but what caused the authors (original and contributing) to arrive at these conclusions and include additional checks and handling code, at the cost of increased complexity? There would appear to be context--likely based on knowledge and direct experience--that is somewhat lost in the example.
A breakdown of the code and decisions behind its parts would likely be very educational for those among us who wouldn't know to include these things by default.
The real world is always messy, especially compared to programming examples on websites. It always helps to have experience with the platforms you're running on, code doesn't run in a vacuum.
When you run a web server, you need a process to listen for connections, and processes for handling the responses (which is handled behind the scenes by the Go standard library here). If you want to listen for signals from the system, you would need another process. This process just sleeps until it gets a signal, then calls the shutdown method on the web server. The main process then stops listening but needs to wait for that routine with shutdown to end. You can think of it as how many things you need to do concurrently, at the same time, and in which order they need to be handled.
This is pretty much how processes are handled in "traditional" unix software, so if you have experience with those platforms and the code running on them, adding this kind of functionality comes naturally.
The question is if you really are going to notice any problems if you wouldn't include graceful shutdown on a typical web server, where connections tend to be short-lived and services are rarely brought down...
if i started this server from the commandline like go run server.go, how do you stop it?
Ctrl-C (sigint), or use kill
to send the process a signal.
how to stop the server once it started?
Anyone know how I can also log GET and POST arguments? Like for example printing out "index.php?firstname=John&lastname=Smith". Would be much appreciated!
Anyone know how I can also log GET and POST arguments? Like for example printing out "index.php?firstname=John&lastname=Smith". Would be much appreciated!
If you are interested in the GET query parameters, check the other fields and methods of URL, such as RequestURI
, RawQuery
or even just calling String()
on URL
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.
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!
Nice! Thanks for sharing!
Question: isn’t it safer to use something like ‘uuid’ for ‘nextRequestID’ or add ‘Mutex.Lock’?