sneak
9f3fb1e944
* updated healthcheck a bit * added maintenance mode config toggle * added recovery handler
264 lines
5.7 KiB
Go
264 lines
5.7 KiB
Go
package httpserver
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/rs/zerolog"
|
|
"github.com/rs/zerolog/log"
|
|
"github.com/spf13/viper"
|
|
|
|
"github.com/getsentry/sentry-go"
|
|
"github.com/go-chi/chi"
|
|
|
|
// spooky action at a distance!
|
|
// this populates the environment
|
|
// from a ./.env file automatically
|
|
// for development configuration.
|
|
// .env contents should be things like
|
|
// `DBURL=postgres://user:pass@.../`
|
|
// (without the backticks, of course)
|
|
_ "github.com/joho/godotenv/autoload"
|
|
)
|
|
|
|
type server struct {
|
|
appname string
|
|
version string
|
|
buildarch string
|
|
databaseURL string
|
|
startupTime time.Time
|
|
port int
|
|
exitCode int
|
|
sentryEnabled bool
|
|
log *zerolog.Logger
|
|
ctx context.Context
|
|
cancelFunc context.CancelFunc
|
|
httpServer *http.Server
|
|
router *chi.Mux
|
|
}
|
|
|
|
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
|
|
})
|
|
|
|
// this does nothing if SENTRY_DSN is unset in env.
|
|
s.enableSentry()
|
|
|
|
// TODO remove:
|
|
if s.sentryEnabled {
|
|
sentry.CaptureMessage("It works!")
|
|
}
|
|
|
|
s.configure()
|
|
s.setupLogging()
|
|
|
|
s.databaseURL = viper.GetString("DBURL")
|
|
s.port = viper.GetInt("PORT")
|
|
|
|
return s.serve()
|
|
}
|
|
|
|
func (s *server) enableSentry() {
|
|
sentryDSN := os.Getenv("SENTRY_DSN")
|
|
if sentryDSN == "" {
|
|
s.sentryEnabled = false
|
|
return
|
|
}
|
|
|
|
err := sentry.Init(sentry.ClientOptions{
|
|
Dsn: sentryDSN,
|
|
Release: fmt.Sprintf("%s-%s", s.appname, s.version),
|
|
})
|
|
if err != nil {
|
|
log.Fatal().Err(err).Msg("sentry init failure")
|
|
}
|
|
s.sentryEnabled = true
|
|
}
|
|
|
|
func (s *server) serve() int {
|
|
s.ctx, s.cancelFunc = context.WithCancel(context.Background())
|
|
|
|
// signal watcher
|
|
go func() {
|
|
c := make(chan os.Signal, 1)
|
|
signal.Ignore(syscall.SIGPIPE)
|
|
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
|
// block and wait for signal
|
|
sig := <-c
|
|
s.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 range s.ctx.Done() {
|
|
//aforementioned clean shutdown upon main context
|
|
//cancellation
|
|
}
|
|
s.cleanShutdown()
|
|
return s.exitCode
|
|
|
|
}
|
|
|
|
func (s *server) cleanupForExit() {
|
|
s.log.Info().Msg("cleaning up")
|
|
// FIXME unimplemented
|
|
// close database connections or whatever
|
|
}
|
|
|
|
func (s *server) cleanShutdown() {
|
|
// initiate clean shutdown
|
|
s.exitCode = 0
|
|
ctxShutdown, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
if err := s.httpServer.Shutdown(ctxShutdown); err != nil {
|
|
s.log.Error().
|
|
Err(err).
|
|
Msg("server clean shutdown failed")
|
|
}
|
|
if shutdownCancel != nil {
|
|
shutdownCancel()
|
|
}
|
|
|
|
s.cleanupForExit()
|
|
|
|
if s.sentryEnabled {
|
|
sentry.Flush(2 * time.Second)
|
|
}
|
|
}
|
|
|
|
func (s *server) uptime() time.Duration {
|
|
return time.Since(s.startupTime)
|
|
}
|
|
|
|
func (s *server) maintenance() bool {
|
|
return viper.GetBool("MAINTENANCE_MODE")
|
|
}
|
|
|
|
func (s *server) configure() {
|
|
viper.SetConfigName(s.appname)
|
|
viper.SetConfigType("yaml")
|
|
// path to look for the config file in:
|
|
viper.AddConfigPath(fmt.Sprintf("/etc/%s", s.appname))
|
|
// call multiple times to add many search paths:
|
|
viper.AddConfigPath(fmt.Sprintf("$HOME/.config/%s", s.appname))
|
|
//viper.SetEnvPrefix(strings.ToUpper(s.appname))
|
|
viper.AutomaticEnv()
|
|
|
|
viper.SetDefault("DEBUG", "false")
|
|
viper.SetDefault("MAINTENANCE_MODE", "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()
|
|
}
|
|
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
|
|
|
tty := false
|
|
if fileInfo, _ := os.Stdout.Stat(); (fileInfo.Mode() & os.ModeCharDevice) != 0 {
|
|
tty = true
|
|
}
|
|
|
|
var writers []io.Writer
|
|
|
|
if tty {
|
|
// this does cool colorization for console/dev
|
|
consoleWriter := zerolog.NewConsoleWriter(
|
|
func(w *zerolog.ConsoleWriter) {
|
|
// Customize time format
|
|
w.TimeFormat = time.RFC3339Nano
|
|
},
|
|
)
|
|
|
|
writers = append(writers, consoleWriter)
|
|
} else {
|
|
// log json in prod for the machines
|
|
writers = append(writers, os.Stdout)
|
|
}
|
|
|
|
/*
|
|
// this is how you log to a file, if you do that
|
|
// sort of thing still
|
|
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()
|
|
|
|
s.log = &logger
|
|
//log.Logger = logger
|
|
|
|
if viper.GetBool("debug") {
|
|
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
|
s.log.Debug().Bool("debug", true).Send()
|
|
}
|
|
|
|
s.identify()
|
|
}
|
|
|
|
func (s *server) identify() {
|
|
s.log.Info().
|
|
Str("appname", s.appname).
|
|
Str("version", s.version).
|
|
Str("buildarch", s.buildarch).
|
|
Msg("starting")
|
|
}
|