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

83
internal/config/config.go Normal file
View File

@@ -0,0 +1,83 @@
package config
import (
"fmt"
"git.eeqj.de/sneak/gohttpserver/internal/globals"
"git.eeqj.de/sneak/gohttpserver/internal/logger"
"github.com/rs/zerolog"
"github.com/spf13/viper"
"go.uber.org/fx"
// 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 ConfigParams struct {
Globals globals.Globals
Logger logger.Logger
}
type Config struct {
DBURL string
Debug bool
MaintenanceMode bool
MetricsPassword string
MetricsUsername string
Port int
SentryDSN string
params ConfigParams
log *zerolog.Logger
}
func New(lc fx.Lifecycle, params ConfigParams) (*Config, error) {
log := params.Logger.Get()
name := params.Globals.Appname
viper.SetConfigName(name)
viper.SetConfigType("yaml")
// path to look for the config file in:
viper.AddConfigPath(fmt.Sprintf("/etc/%s", name))
// call multiple times to add many search paths:
viper.AddConfigPath(fmt.Sprintf("$HOME/.config/%s", name))
// 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")
}
}
s := &Config{
DBURL: viper.GetString("DBURL"),
Debug: viper.GetBool("debug"),
Port: viper.GetInt("PORT"),
SentryDSN: viper.GetString("SENTRY_DSN"),
MaintenanceMode: viper.GetBool("MAINTENANCE_MODE"),
MetricsUsername: viper.GetString("METRICS_USERNAME"),
MetricsPassword: viper.GetString("METRICS_PASSWORD"),
log: log,
}
return s, nil
}

View File

@@ -0,0 +1,49 @@
package database
import (
"context"
"git.eeqj.de/sneak/gohttpserver/internal/config"
"git.eeqj.de/sneak/gohttpserver/internal/logger"
"github.com/rs/zerolog"
"go.uber.org/fx"
// 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 DatabaseParams struct {
fx.In
Logger logger.Logger
Config config.Config
}
type Database struct {
URL string
log *zerolog.Logger
params DatabaseParams
}
func New(lc fx.Lifecycle, params DatabaseParams) (*Database, error) {
s.log.Info().Msg("Database instantiated")
s := new(Database)
s.params = params
s.log = params.Logger.Get()
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
s.log.Info().Msg("Database OnStart Hook")
// FIXME connect to db
},
OnStop: func(ctx context.Context) error {
// FIXME disconnect from db
},
})
return s, nil
}

View File

@@ -0,0 +1,27 @@
package globals
import (
"go.uber.org/fx"
)
// these get populated from main() and copied into the Globals object.
var (
Appname string
Version string
Buildarch string
)
type Globals struct {
Appname string
Version string
Buildarch string
}
func New(lc fx.Lifecycle) (*Globals, error) {
n := &Globals{
Appname: Appname,
Buildarch: Buildarch,
Version: Version,
}
return n, nil
}

View File

@@ -1,11 +1,11 @@
package server
package handlers
import (
"net/http"
"time"
)
func (s *server) handleHealthCheck() http.HandlerFunc {
func (s *Handlers) handleHealthCheck() http.HandlerFunc {
type response struct {
Status string `json:"status"`
Now string `json:"now"`

View File

@@ -1,20 +1,19 @@
package server
package handlers
import (
"html/template"
"log"
"net/http"
"git.eeqj.de/sneak/gohttpserver/templates"
)
func (s *server) handleIndex() http.HandlerFunc {
func (s *Handlers) 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())
s.log.Println(err.Error())
http.Error(w, http.StatusText(500), 500)
}
}

View File

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

View File

@@ -1,11 +1,11 @@
package server
package handlers
import (
"net/http"
"time"
)
func (s *server) handleNow() http.HandlerFunc {
func (s *Handlers) HandleNow() http.HandlerFunc {
type response struct {
Now time.Time `json:"now"`
}

View File

@@ -1,4 +1,4 @@
package server
package handlers
import (
"net/http"
@@ -7,7 +7,7 @@ import (
// 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 {
func (s *Handlers) HandlePanic() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
panic("y tho")
}

View File

@@ -0,0 +1,61 @@
package handlers
import (
"context"
"encoding/json"
"net/http"
"time"
"git.eeqj.de/sneak/gohttpserver/internal/globals"
"github.com/rs/zerolog"
"go.uber.org/fx"
"google.golang.org/genproto/googleapis/spanner/admin/database/v1"
)
type HandlersParams struct {
fx.In
Logger *zerolog.Logger
Globals globals.Globals
Database database.Database
}
type Handlers struct {
params HandlersParams
log *zerolog.Logger
}
func New(lc fx.Lifecycle, params HandlersParams) (*Handlers, error) {
s := new(Handlers)
s.params = params
s.log = params.Logger.Get()
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
// FIXME compile some templates here or something
},
})
return s, nil
}
func (s *Handlers) 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)
}
}
func (s *Handlers) 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 *Handlers) decodeJSON(w http.ResponseWriter, r *http.Request, v interface{}) error { // nolint
return json.NewDecoder(r.Body).Decode(v)
}

99
internal/logger/logger.go Normal file
View File

@@ -0,0 +1,99 @@
package server
import (
"io"
"os"
"time"
"git.eeqj.de/sneak/gohttpserver/internal/globals"
"github.com/rs/zerolog"
"go.uber.org/fx"
"honnef.co/go/tools/config"
)
type LoggerParams struct {
fx.In
Globals globals.Globals
Config config.Config
}
type Logger struct {
log *zerolog.Logger
params LoggerParams
}
func New(lc fx.Lifecycle, params LoggerParams) (*Logger, error) {
l := new(Logger)
// 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()
l.log = &logger
// log.Logger = logger
if l.params.Config.Debug {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
l.log.Debug().Bool("debug", true).Send()
}
return l, nil
}
func (l *Logger) Get() *zerolog.Logger {
return l.log
}
func (l *Logger) Identify() {
l.log.Info().
Str("appname", l.params.Globals.Appname).
Str("version", l.params.Globals.Version).
Str("buildarch", l.params.Globals.Buildarch).
Msg("starting")
}

View File

@@ -45,7 +45,7 @@ func (lrw *loggingResponseWriter) WriteHeader(code int) {
// 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 {
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) {
@@ -73,7 +73,7 @@ func (s *server) LoggingMiddleware() func(http.Handler) http.Handler {
}
}
func (s *server) CORSMiddleware() func(http.Handler) http.Handler {
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.
@@ -88,7 +88,7 @@ func (s *server) CORSMiddleware() func(http.Handler) http.Handler {
})
}
func (s *server) AuthMiddleware() func(http.Handler) http.Handler {
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.
@@ -98,7 +98,7 @@ func (s *server) AuthMiddleware() func(http.Handler) http.Handler {
}
}
func (s *server) MetricsMiddleware() func(http.Handler) http.Handler {
func (s *Server) MetricsMiddleware() func(http.Handler) http.Handler {
mdlw := ghmm.New(ghmm.Config{
Recorder: metrics.NewRecorder(metrics.Config{}),
})
@@ -107,7 +107,7 @@ func (s *server) MetricsMiddleware() func(http.Handler) http.Handler {
}
}
func (s *server) MetricsAuthMiddleware() func(http.Handler) http.Handler {
func (s *Server) MetricsAuthMiddleware() func(http.Handler) http.Handler {
return basicauth.New(
"metrics",
map[string][]string{

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

@@ -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())
// }
}