this is nowhere near working yet
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
2022-11-28 04:59:20 +01:00
parent 0c3797ec30
commit 75442d261d
19 changed files with 567 additions and 308 deletions

View File

@@ -1,30 +0,0 @@
package server
import (
"net/http"
"time"
)
func (s *server) handleHealthCheck() http.HandlerFunc {
type response struct {
Status string `json:"status"`
Now string `json:"now"`
UptimeSeconds int64 `json:"uptime_seconds"`
UptimeHuman string `json:"uptime_human"`
Version string `json:"version"`
Appname string `json:"appname"`
Maintenance bool `json:"maintenance_mode"`
}
return func(w http.ResponseWriter, req *http.Request) {
resp := &response{
Status: "ok",
Now: time.Now().UTC().Format(time.RFC3339Nano),
UptimeSeconds: int64(s.uptime().Seconds()),
UptimeHuman: s.uptime().String(),
Maintenance: s.maintenance(),
Appname: s.appname,
Version: s.version,
}
s.respondJSON(w, req, resp, 200)
}
}

View File

@@ -1,21 +0,0 @@
package server
import (
"html/template"
"log"
"net/http"
"git.eeqj.de/sneak/gohttpserver/templates"
)
func (s *server) handleIndex() http.HandlerFunc {
indexTemplate := template.Must(template.New("index").Parse(templates.MustString("index.html")))
return func(w http.ResponseWriter, r *http.Request) {
err := indexTemplate.ExecuteTemplate(w, "index", nil)
if err != nil {
log.Println(err.Error())
http.Error(w, http.StatusText(500), 500)
}
}
}

View File

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

View File

@@ -1,14 +0,0 @@
package server
import (
"net/http"
)
// CHANGEME you probably want to remove this,
// this is just a handler/route that throws a panic to test
// sentry events.
func (s *server) handlePanic() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
panic("y tho")
}
}

View File

@@ -1,15 +0,0 @@
package server
import (
"net/http"
"time"
)
func (s *server) handleNow() http.HandlerFunc {
type response struct {
Now time.Time `json:"now"`
}
return func(w http.ResponseWriter, r *http.Request) {
s.respondJSON(w, r, &response{Now: time.Now()}, 200)
}
}

View File

@@ -1,13 +1,12 @@
package server
import (
"encoding/json"
"fmt"
"net/http"
"time"
)
func (s *server) serveUntilShutdown() {
func (s *Server) serveUntilShutdown() {
listenAddr := fmt.Sprintf(":%d", s.port)
s.httpServer = &http.Server{
Addr: listenAddr,
@@ -30,21 +29,6 @@ func (s *server) serveUntilShutdown() {
}
}
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 {
s.log.Error().Err(err).Msg("json encode error")
}
}
}
func (s *server) decodeJSON(w http.ResponseWriter, r *http.Request, v interface{}) error { // nolint
return json.NewDecoder(r.Body).Decode(v)
}
func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.router.ServeHTTP(w, r)
}

View File

@@ -1,269 +0,0 @@
package server
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
}
// FIXME change this to use uber/fx DI and an Invoke()
// 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.
// TODO remove:
if s.sentryEnabled {
sentry.CaptureMessage("It works!")
}
s.configure()
s.setupLogging()
// logging before sentry, because sentry logs
s.enableSentry()
s.databaseURL = viper.GetString("DBURL")
s.port = viper.GetInt("PORT")
return s.serve()
}
func (s *server) enableSentry() {
s.sentryEnabled = false
if viper.GetString("SENTRY_DSN") == "" {
return
}
err := sentry.Init(sentry.ClientOptions{
Dsn: viper.GetString("SENTRY_DSN"),
Release: fmt.Sprintf("%s-%s", s.appname, s.version),
})
if err != nil {
log.Fatal().Err(err).Msg("sentry init failure")
return
}
s.log.Info().Msg("sentry error reporting activated")
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", "")
viper.SetDefault("SENTRY_DSN", "")
viper.SetDefault("METRICS_USERNAME", "")
viper.SetDefault("METRICS_PASSWORD", "")
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")
}

View File

@@ -1,119 +0,0 @@
package server
import (
"net"
"net/http"
"time"
basicauth "github.com/99designs/basicauth-go"
"github.com/go-chi/chi/middleware"
"github.com/go-chi/cors"
metrics "github.com/slok/go-http-metrics/metrics/prometheus"
ghmm "github.com/slok/go-http-metrics/middleware"
"github.com/slok/go-http-metrics/middleware/std"
"github.com/spf13/viper"
)
// the following is from
// https://learning-cloud-native-go.github.io/docs/a6.adding_zerolog_logger/
func ipFromHostPort(hp string) string {
h, _, err := net.SplitHostPort(hp)
if err != nil {
return ""
}
if len(h) > 0 && h[0] == '[' {
return h[1 : len(h)-1]
}
return h
}
type loggingResponseWriter struct {
http.ResponseWriter
statusCode int
}
func NewLoggingResponseWriter(w http.ResponseWriter) *loggingResponseWriter {
return &loggingResponseWriter{w, http.StatusOK}
}
func (lrw *loggingResponseWriter) WriteHeader(code int) {
lrw.statusCode = code
lrw.ResponseWriter.WriteHeader(code)
}
// type Middleware func(http.Handler) http.Handler
// this returns a Middleware that is designed to do every request through the
// mux, note the signature:
func (s *server) LoggingMiddleware() func(http.Handler) http.Handler {
// FIXME this should use https://github.com/google/go-cloud/blob/master/server/requestlog/requestlog.go
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
lrw := NewLoggingResponseWriter(w)
ctx := r.Context()
defer func() {
latency := time.Since(start)
s.log.Info().
Time("request_start", start).
Str("method", r.Method).
Str("url", r.URL.String()).
Str("useragent", r.UserAgent()).
Str("request_id", ctx.Value(middleware.RequestIDKey).(string)).
Str("referer", r.Referer()).
Str("proto", r.Proto).
Str("remoteIP", ipFromHostPort(r.RemoteAddr)).
Int("status", lrw.statusCode).
Int("latency_ms", int(latency.Milliseconds())).
Send()
}()
next.ServeHTTP(lrw, r)
})
}
}
func (s *server) CORSMiddleware() func(http.Handler) http.Handler {
return cors.Handler(cors.Options{
// CHANGEME! these are defaults, change them to suit your needs or
// read from environment/viper.
// AllowedOrigins: []string{"https://foo.com"}, // Use this to allow specific origin hosts
AllowedOrigins: []string{"*"},
// AllowOriginFunc: func(r *http.Request, origin string) bool { return true },
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
ExposedHeaders: []string{"Link"},
AllowCredentials: false,
MaxAge: 300, // Maximum value not ignored by any of major browsers
})
}
func (s *server) AuthMiddleware() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// CHANGEME you'll want to change this to do stuff.
s.log.Info().Msg("AUTH: before request")
next.ServeHTTP(w, r)
})
}
}
func (s *server) MetricsMiddleware() func(http.Handler) http.Handler {
mdlw := ghmm.New(ghmm.Config{
Recorder: metrics.NewRecorder(metrics.Config{}),
})
return func(next http.Handler) http.Handler {
return std.Handler("", mdlw, next)
}
}
func (s *server) MetricsAuthMiddleware() func(http.Handler) http.Handler {
return basicauth.New(
"metrics",
map[string][]string{
viper.GetString("METRICS_USERNAME"): {
viper.GetString("METRICS_PASSWORD"),
},
},
)
}

View File

@@ -12,7 +12,7 @@ import (
"github.com/spf13/viper"
)
func (s *server) routes() {
func (s *Server) SetupRoutes() {
s.router = chi.NewRouter()
// the mux .Use() takes a http.Handler wrapper func, like most

178
internal/server/server.go Normal file
View File

@@ -0,0 +1,178 @@
package server
import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"git.eeqj.de/sneak/gohttpserver/internal/globals"
"github.com/docker/docker/daemon/logger"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
"go.uber.org/fx"
"honnef.co/go/tools/config"
"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 ServerParams struct {
fx.In
Logger logger.Logger
Globals globals.Globals
Config config.Config
}
type Server struct {
appname string
version string
buildarch 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
params ServerParams
}
func New(lc fx.Lifecycle, params ServerParams) (*Server, error) {
s := new(Server)
s.params = params
s.log = params.Logger.Get()
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
s.startupTime = time.Now()
s.version = params.Globals.Version
s.Run()
},
OnStop: func(ctx context.Context) error {
// FIXME do server shutdown here
},
})
return s, nil
}
// FIXME change this to use uber/fx DI and an Invoke()
// this is where we come in from package main.
func (s *Server) Run() {
// this does nothing if SENTRY_DSN is unset in env.
// TODO remove:
if s.sentryEnabled {
sentry.CaptureMessage("It works!")
}
s.configure()
// logging before sentry, because sentry logs
s.enableSentry()
return s.serve()
}
func (s *Server) enableSentry() {
s.sentryEnabled = false
if s.Config.SentryDSN == "" {
return
}
err := sentry.Init(sentry.ClientOptions{
Dsn: viper.GetString("SENTRY_DSN"),
Release: fmt.Sprintf("%s-%s", s.params.Globals.Appname, s.params.Globals.Version),
})
if err != nil {
log.Fatal().Err(err).Msg("sentry init failure")
return
}
s.log.Info().Msg("sentry error reporting activated")
s.sentryEnabled = true
}
func (s *Server) serve() int {
// FIXME fx will handle this for us
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 s.params.Config.MaintenanceMode
}
func (s *Server) configure() {
// FIXME move most of this to dedicated places
// if viper.GetBool("DEBUG") {
// pp.Print(viper.AllSettings())
// }
}