package main import ( "context" "flag" "fmt" "log" "net/http" "os" "os/signal" "sync/atomic" "time" "github.com/ncarlier/webhookd/pkg/api" "github.com/ncarlier/webhookd/pkg/logger" "github.com/ncarlier/webhookd/pkg/worker" ) type key int const ( requestIDKey key = 0 ) var ( healthy int32 ) var ( listenAddr = flag.String("l", ":8080", "HTTP service address (e.g.address, ':8080')") nbWorkers = flag.Int("n", 2, "The number of workers to start") debug = flag.Bool("d", false, "Output debug logs") ) func main() { flag.Parse() level := "info" if *debug { level = "debug" } logger.Init(level) logger.Debug.Println("Starting webhookd server...") router := http.NewServeMux() router.Handle("/", api.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.Debug)(router)), ErrorLog: logger.Error, ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, IdleTimeout: 15 * time.Second, } // Start the dispatcher. logger.Debug.Printf("Starting the dispatcher (%d workers)...\n", *nbWorkers) worker.StartDispatcher(*nbWorkers) done := make(chan bool) quit := make(chan os.Signal, 1) signal.Notify(quit, os.Interrupt) go func() { <-quit logger.Debug.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.Error.Fatalf("Could not gracefully shutdown the server: %v\n", err) } close(done) }() logger.Info.Println("Server is ready to handle requests at", *listenAddr) atomic.StoreInt32(&healthy, 1) if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { logger.Error.Fatalf("Could not listen on %s: %v\n", *listenAddr, err) } <-done logger.Debug.Println("Server stopped") } 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)) }) } }