This commit is contained in:
2020-09-29 23:35:07 -07:00
commit eaa2f2b929
13 changed files with 785 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
package httpserver
import (
"fmt"
"net/http"
"time"
)
func (s *server) handleHealthCheck() http.HandlerFunc {
type response struct {
Status string `json:"status"`
Now string `json:"now"`
Uptime string `json:"uptime"`
Version string `json:"version"`
Appname string `json:"appname"`
}
return func(w http.ResponseWriter, req *http.Request) {
resp := &response{
Status: "ok",
Now: time.Now().UTC().Format(time.RFC3339Nano),
Uptime: fmt.Sprintf("%f", s.uptime().Seconds()),
Appname: s.appname,
Version: s.version,
}
s.respondJSON(w, req, resp, 200)
}
}

12
httpserver/handleindex.go Normal file
View File

@@ -0,0 +1,12 @@
package httpserver
import (
"fmt"
"net/http"
)
func (s *server) handleIndex() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hello world")
}
}

72
httpserver/http.go Normal file
View File

@@ -0,0 +1,72 @@
package httpserver
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/rs/zerolog/log"
)
func (s *server) serveUntilShutdown() {
listenAddr := fmt.Sprintf(":%d", s.port)
s.httpServer = &http.Server{
Addr: listenAddr,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
Handler: s,
}
// add routes
// this does any necessary setup in each handler
s.routes()
log.Info().Str("listenaddr", listenAddr).Msg("http begin listen")
if err := s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Error().Msgf("listen:%+s\n", err)
if s.cancelFunc != nil {
s.cancelFunc()
}
}
}
func (s *server) respondJSON(w http.ResponseWriter, r *http.Request, data interface{}, status int) {
w.WriteHeader(status)
w.Header().Set("Content-Type", "application/json")
if data != nil {
err := json.NewEncoder(w).Encode(data)
if err != nil {
log.Error().Err(err).Msg("json encode error")
}
}
}
func (s *server) decodeJSON(w http.ResponseWriter, r *http.Request, v interface{}) error {
return json.NewDecoder(r.Body).Decode(v)
}
func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// FIXME logging or any other general purpose middleware here
s.router.ServeHTTP(w, r)
}
func (s *server) cleanupForExit() {
log.Info().Msg("cleaning up")
// FIXME unimplemented
// close database connections or whatever
}
func (s *server) cleanShutdown() {
// initiate clean shutdown
s.exitCode = 0
ctxShutdown, _ := context.WithTimeout(context.Background(), 5*time.Second)
if err := s.httpServer.Shutdown(ctxShutdown); err != nil {
log.Error().
Err(err).
Msg("server clean shutdown failed")
}
s.cleanupForExit()
}

195
httpserver/main.go Normal file
View File

@@ -0,0 +1,195 @@
package httpserver
import (
"context"
"fmt"
"io"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gorilla/mux"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
)
type server struct {
appname string
version string
buildarch string
databaseURL string
startupTime time.Time
port int
exitCode int
log *zerolog.Logger
ctx context.Context
cancelFunc context.CancelFunc
httpServer *http.Server
router *mux.Router
}
func NewServer(options ...func(s *server)) *server {
n := new(server)
n.startupTime = time.Now()
n.version = "unknown"
for _, opt := range options {
opt(n)
}
return n
}
// this is where we come in from package main.
func Run(appname, version, buildarch string) int {
s := NewServer(func(i *server) {
i.appname = appname
if version != "" {
i.version = version
}
i.buildarch = buildarch
})
s.configure()
s.setupLogging()
s.databaseURL = viper.GetString("DBURL")
s.port = viper.GetInt("PORT")
return s.serve()
}
func (s *server) serve() int {
s.ctx, s.cancelFunc = context.WithCancel(context.Background())
// signal watcher
go func() {
c := make(chan os.Signal)
signal.Ignore(syscall.SIGPIPE)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
// block and wait for signal
sig := <-c
log.Info().Msgf("signal received: %+v", sig)
if s.cancelFunc != nil {
// cancelling the main context will trigger a clean
// shutdown.
s.cancelFunc()
}
}()
go s.serveUntilShutdown()
for {
select {
case <-s.ctx.Done():
//aforementioned clean shutdown upon main context
//cancellation
s.cleanShutdown()
return s.exitCode
}
}
}
func (s *server) uptime() time.Duration {
return time.Since(s.startupTime)
}
func (s *server) configure() {
viper.SetConfigName(s.appname)
viper.SetConfigType("yaml")
viper.AddConfigPath(fmt.Sprintf("/etc/%s", s.appname)) // path to look for the config file in
viper.AddConfigPath(fmt.Sprintf("$HOME/.config/%s", s.appname)) // call multiple times to add many search paths
//viper.SetEnvPrefix(strings.ToUpper(s.appname))
viper.AutomaticEnv()
viper.SetDefault("DEBUG", "false")
viper.SetDefault("PORT", "8080")
viper.SetDefault("DBURL", "")
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
// Config file not found; ignore error if desired
} else {
// Config file was found but another error was produced
log.Panic().
Err(err).
Msg("config file malformed")
}
}
// if viper.GetBool("debug") {
// pp.Print(viper.AllSettings())
// }
}
func (s *server) setupLogging() {
// always log in UTC
zerolog.TimestampFunc = func() time.Time {
return time.Now().UTC()
}
log.Logger = log.With().Caller().Logger()
zerolog.SetGlobalLevel(zerolog.InfoLevel)
tty := false
if fileInfo, _ := os.Stdout.Stat(); (fileInfo.Mode() & os.ModeCharDevice) != 0 {
tty = true
}
log.Info().Bool("tty", tty).Msg("tty detection")
var writers []io.Writer
if tty {
consoleWriter := zerolog.NewConsoleWriter(
func(w *zerolog.ConsoleWriter) {
// Customize time format
w.TimeFormat = time.RFC3339Nano
},
)
writers = append(writers, consoleWriter)
} else {
writers = append(writers, os.Stdout)
}
/*
logfile := viper.GetString("Logfile")
if logfile != "" {
logfileDir := filepath.Dir(logfile)
err := goutil.Mkdirp(logfileDir)
if err != nil {
log.Error().Err(err).Msg("unable to create log dir")
}
hp.logfh, err = os.OpenFile(logfile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
panic("unable to open logfile: " + err.Error())
}
writers = append(writers, hp.logfh)
}
*/
multi := zerolog.MultiLevelWriter(writers...)
logger := zerolog.New(multi).With().Timestamp().Logger().With().Caller().Logger()
log.Logger = logger
if viper.GetBool("debug") {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
log.Debug().Bool("debug", true).Send()
}
s.identify()
}
func (s *server) identify() {
log.Info().
Str("appname", s.appname).
Str("version", s.version).
Str("buildarch", s.buildarch).
Msg("starting")
}

9
httpserver/routes.go Normal file
View File

@@ -0,0 +1,9 @@
package httpserver
import "github.com/gorilla/mux"
func (s *server) routes() {
s.router = mux.NewRouter()
s.router.HandleFunc("/", s.handleIndex()).Methods("GET")
s.router.HandleFunc("/.well-known/healthcheck.json", s.handleHealthCheck()).Methods("GET")
}