sneak/integrate-di (#17)
moving this to use uber/fx di framework instead of the ad hoc di setup before Co-authored-by: sneak <sneak@sneak.berlin> Reviewed-on: #17master
parent
0c3797ec30
commit
dd778174a7
@ -1,3 +1,4 @@ |
||||
/httpd |
||||
debug.log |
||||
/.env |
||||
/cmd/httpd/httpd |
||||
|
@ -0,0 +1,90 @@ |
||||
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 { |
||||
fx.In |
||||
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, |
||||
params: ¶ms, |
||||
} |
||||
|
||||
if s.Debug { |
||||
params.Logger.EnableDebugLogging() |
||||
s.log = params.Logger.Get() |
||||
} |
||||
|
||||
return s, nil |
||||
} |
@ -0,0 +1,52 @@ |
||||
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 := new(Database) |
||||
s.params = ¶ms |
||||
s.log = params.Logger.Get() |
||||
|
||||
s.log.Info().Msg("Database instantiated") |
||||
|
||||
lc.Append(fx.Hook{ |
||||
OnStart: func(ctx context.Context) error { |
||||
s.log.Info().Msg("Database OnStart Hook") |
||||
// FIXME connect to db
|
||||
return nil |
||||
}, |
||||
OnStop: func(ctx context.Context) error { |
||||
// FIXME disconnect from db
|
||||
return nil |
||||
}, |
||||
}) |
||||
return s, nil |
||||
} |
@ -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 |
||||
} |
@ -0,0 +1,57 @@ |
||||
package handlers |
||||
|
||||
import ( |
||||
"context" |
||||
"encoding/json" |
||||
"net/http" |
||||
|
||||
"git.eeqj.de/sneak/gohttpserver/internal/database" |
||||
"git.eeqj.de/sneak/gohttpserver/internal/globals" |
||||
"git.eeqj.de/sneak/gohttpserver/internal/healthcheck" |
||||
"git.eeqj.de/sneak/gohttpserver/internal/logger" |
||||
"github.com/rs/zerolog" |
||||
"go.uber.org/fx" |
||||
) |
||||
|
||||
type HandlersParams struct { |
||||
fx.In |
||||
Logger *logger.Logger |
||||
Globals *globals.Globals |
||||
Database *database.Database |
||||
Healthcheck *healthcheck.Healthcheck |
||||
} |
||||
|
||||
type Handlers struct { |
||||
params *HandlersParams |
||||
log *zerolog.Logger |
||||
hc *healthcheck.Healthcheck |
||||
} |
||||
|
||||
func New(lc fx.Lifecycle, params HandlersParams) (*Handlers, error) { |
||||
s := new(Handlers) |
||||
s.params = ¶ms |
||||
s.log = params.Logger.Get() |
||||
s.hc = params.Healthcheck |
||||
lc.Append(fx.Hook{ |
||||
OnStart: func(ctx context.Context) error { |
||||
// FIXME compile some templates here or something
|
||||
return nil |
||||
}, |
||||
}) |
||||
return s, nil |
||||
} |
||||
|
||||
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) |
||||
} |
@ -0,0 +1,12 @@ |
||||
package handlers |
||||
|
||||
import ( |
||||
"net/http" |
||||
) |
||||
|
||||
func (s *Handlers) HandleHealthCheck() http.HandlerFunc { |
||||
return func(w http.ResponseWriter, req *http.Request) { |
||||
resp := s.hc.Healthcheck() |
||||
s.respondJSON(w, req, resp, 200) |
||||
} |
||||
} |
@ -0,0 +1,19 @@ |
||||
package handlers |
||||
|
||||
import ( |
||||
"net/http" |
||||
|
||||
"git.eeqj.de/sneak/gohttpserver/templates" |
||||
) |
||||
|
||||
func (s *Handlers) HandleIndex() http.HandlerFunc { |
||||
t := templates.GetParsed() |
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) { |
||||
err := t.ExecuteTemplate(w, "index.html", nil) |
||||
if err != nil { |
||||
s.log.Error().Err(err).Msg("") |
||||
http.Error(w, http.StatusText(500), 500) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,19 @@ |
||||
package handlers |
||||
|
||||
import ( |
||||
"net/http" |
||||
|
||||
"git.eeqj.de/sneak/gohttpserver/templates" |
||||
) |
||||
|
||||
func (s *Handlers) HandleLoginGET() http.HandlerFunc { |
||||
t := templates.GetParsed() |
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) { |
||||
err := t.ExecuteTemplate(w, "login.html", nil) |
||||
if err != nil { |
||||
s.log.Error().Err(err).Msg("") |
||||
http.Error(w, http.StatusText(500), 500) |
||||
} |
||||
} |
||||
} |
@ -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"` |
||||
} |
@ -0,0 +1,71 @@ |
||||
package healthcheck |
||||
|
||||
import ( |
||||
"context" |
||||
"time" |
||||
|
||||
"git.eeqj.de/sneak/gohttpserver/internal/config" |
||||
"git.eeqj.de/sneak/gohttpserver/internal/database" |
||||
"git.eeqj.de/sneak/gohttpserver/internal/globals" |
||||
"git.eeqj.de/sneak/gohttpserver/internal/logger" |
||||
"github.com/rs/zerolog" |
||||
"go.uber.org/fx" |
||||
) |
||||
|
||||
type HealthcheckParams struct { |
||||
fx.In |
||||
Globals *globals.Globals |
||||
Config *config.Config |
||||
Logger *logger.Logger |
||||
Database *database.Database |
||||
} |
||||
|
||||
type Healthcheck struct { |
||||
StartupTime time.Time |
||||
log *zerolog.Logger |
||||
params *HealthcheckParams |
||||
} |
||||
|
||||
func New(lc fx.Lifecycle, params HealthcheckParams) (*Healthcheck, error) { |
||||
s := new(Healthcheck) |
||||
s.params = ¶ms |
||||
s.log = params.Logger.Get() |
||||
|
||||
lc.Append(fx.Hook{ |
||||
OnStart: func(ctx context.Context) error { |
||||
s.StartupTime = time.Now() |
||||
return nil |
||||
}, |
||||
OnStop: func(ctx context.Context) error { |
||||
// FIXME do server shutdown here
|
||||
return nil |
||||
}, |
||||
}) |
||||
return s, nil |
||||
} |
||||
|
||||
type HealthcheckResponse 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"` |
||||
} |
||||
|
||||
func (s *Healthcheck) uptime() time.Duration { |
||||
return time.Since(s.StartupTime) |
||||
} |
||||
|
||||
func (s *Healthcheck) Healthcheck() *HealthcheckResponse { |
||||
resp := &HealthcheckResponse{ |
||||
Status: "ok", |
||||
Now: time.Now().UTC().Format(time.RFC3339Nano), |
||||
UptimeSeconds: int64(s.uptime().Seconds()), |
||||
UptimeHuman: s.uptime().String(), |
||||
Appname: s.params.Globals.Appname, |
||||
Version: s.params.Globals.Version, |
||||
} |
||||
return resp |
||||
} |
@ -0,0 +1,97 @@ |
||||
package logger |
||||
|
||||
import ( |
||||
"io" |
||||
"os" |
||||
"time" |
||||
|
||||
"git.eeqj.de/sneak/gohttpserver/internal/globals" |
||||
"github.com/rs/zerolog" |
||||
"go.uber.org/fx" |
||||
) |
||||
|
||||
type LoggerParams struct { |
||||
fx.In |
||||
Globals *globals.Globals |
||||
} |
||||
|
||||
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
|
||||
|
||||
return l, nil |
||||
} |
||||
|
||||
func (l *Logger) EnableDebugLogging() { |
||||
zerolog.SetGlobalLevel(zerolog.DebugLevel) |
||||
l.log.Debug().Bool("debug", true).Send() |
||||
} |
||||
|
||||
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") |
||||
} |
@ -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) |
||||
} |
||||
} |
@ -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) |
||||
} |
||||
} |
||||
} |
@ -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") |
||||
} |
||||
} |
@ -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") |
||||
} |
@ -0,0 +1,178 @@ |
||||
package server |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
"net/http" |
||||
"os" |
||||
"os/signal" |
||||
"syscall" |
||||
"time" |
||||
|
||||
"git.eeqj.de/sneak/gohttpserver/internal/config" |
||||
"git.eeqj.de/sneak/gohttpserver/internal/globals" |
||||
"git.eeqj.de/sneak/gohttpserver/internal/handlers" |
||||
"git.eeqj.de/sneak/gohttpserver/internal/logger" |
||||
"git.eeqj.de/sneak/gohttpserver/internal/middleware" |
||||
"github.com/rs/zerolog" |
||||
"go.uber.org/fx" |
||||
|
||||
"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 |
||||
Middleware *middleware.Middleware |
||||
Handlers *handlers.Handlers |
||||
} |
||||
|
||||
type Server struct { |
||||
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 |
||||
mw *middleware.Middleware |
||||
h *handlers.Handlers |
||||
} |
||||
|
||||
func New(lc fx.Lifecycle, params ServerParams) (*Server, error) { |
||||
s := new(Server) |
||||
s.params = params |
||||
s.mw = params.Middleware |
||||
s.h = params.Handlers |
||||
s.log = params.Logger.Get() |
||||
|
||||
lc.Append(fx.Hook{ |
||||
OnStart: func(ctx context.Context) error { |
||||
s.startupTime = time.Now() |
||||
go s.Run() // background FIXME
|
||||
return nil |
||||
}, |
||||
OnStop: func(ctx context.Context) error { |
||||
// FIXME do server shutdown here
|
||||
return nil |
||||
}, |
||||
}) |
||||
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() |
||||
|
||||
s.serve() // FIXME deal with return value
|
||||
} |
||||
|
||||
func (s *Server) enableSentry() { |
||||
s.sentryEnabled = false |
||||
|
||||
if s.params.Config.SentryDSN == "" { |
||||
return |
||||
} |
||||
|
||||
err := sentry.Init(sentry.ClientOptions{ |
||||
Dsn: s.params.Config.SentryDSN, |
||||
Release: fmt.Sprintf("%s-%s", s.params.Globals.Appname, s.params.Globals.Version), |
||||
}) |
||||
if err != nil { |
||||
s.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) MaintenanceMode() 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())
|
||||
// }
|
||||
} |
@ -0,0 +1,3 @@ |
||||
<script src="/s/js/jquery-3.5.1.slim.min.js"></script> |
||||
<script src="/s/js/bootstrap-4.5.3.bundle.min.js"></script> |
||||
<script src="/s/js/main.js"></script> |
@ -0,0 +1,4 @@ |
||||
<meta charset="utf-8" /> |
||||
<title>{{ .HTMLTitle }}</title> |
||||
<link rel="stylesheet" href="/s/css/bootstrap-4.5.3.min.css" /> |
||||
<link rel="stylesheet" href="/s/css/style.css" /> |
@ -0,0 +1,64 @@ |
||||
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark"> |
||||
<a class="navbar-brand" href="/">{{.SiteName}}</a> |
||||
<button |
||||
class="navbar-toggler" |
||||
type="button" |
||||
data-toggle="collapse" |
||||
data-target="#navbarsExampleDefault" |
||||
aria-controls="navbarsExampleDefault" |
||||
aria-expanded="false" |
||||
aria-label="Toggle navigation" |
||||
> |
||||
<span class="navbar-toggler-icon"> </span> |
||||
</button> |
||||
|
||||
<div class="collapse navbar-collapse" id="navbarsExampleDefault"> |
||||
<ul class="navbar-nav mr-auto"> |
||||
<li class="nav-item active"> |
||||
<a class="nav-link" href="#" |
||||
>Home <span class="sr-only">(current)</span></a |
||||
> |
||||
</li> |
||||
<li class="nav-item"> |
||||
<a class="nav-link" href="#">Link</a> |
||||
</li> |
||||
<li class="nav-item"> |
||||
<a class="nav-link disabled" href="#" aria-disabled="true" |
||||
>Disabled</a |
||||
> |
||||
</li> |
||||
<li class="nav-item dropdown"> |
||||
<a |
||||
class="nav-link dropdown-toggle" |
||||
href="#" |
||||
id="dropdown01" |
||||
data-toggle="dropdown" |
||||
aria-haspopup="true" |
||||
aria-expanded="false" |
||||
>Dropdown</a |
||||
> |
||||
<div class="dropdown-menu" aria-labelledby="dropdown01"> |
||||
<a class="dropdown-item" href="#">Action</a> |
||||
<a class="dropdown-item" href="#">Another action</a> |
||||
<a class="dropdown-item" href="#" |
||||
>Something else here</a |
||||
> |
||||
</div> |
||||
</li> |
||||
</ul> |
||||
<form action="POST" class="form-inline my-2 my-lg-0"> |
||||
<input |
||||
class="form-control mr-sm-2" |
||||
type="text" |
||||
placeholder="Search" |
||||
aria-label="Search" |
||||
/> |
||||
<button |
||||
class="btn btn-outline-success my-2 my-sm-0" |
||||
type="submit" |
||||
> |
||||
Search |
||||
</button> |
||||
</form> |
||||
</div> |
||||
</nav> |
@ -0,0 +1,3 @@ |
||||
<footer class="container"> |
||||
<p>© No rights reserved - This is in the public domain!</p> |
||||
</footer> |
@ -0,0 +1,76 @@ |
||||
<!DOCTYPE html> |
||||
<html lang="en"> |
||||
<head> |
||||
{{ template "htmlheader.html" .HTMLHeader }} |
||||
<style> |
||||
body { |
||||
padding-top: 3.5rem; |
||||
} |
||||
|
||||
.bd-placeholder-img { |
||||
font-size: 1.125rem; |
||||
text-anchor: middle; |
||||
-webkit-user-select: none; |
||||
-moz-user-select: none; |
||||
-ms-user-select: none; |
||||
user-select: none; |
||||
} |
||||
|
||||
@media (min-width: 768px) { |
||||
.bd-placeholder-img-lg { |
||||
font-size: 3.5rem; |
||||
} |
||||
} |
||||
</style> |
||||
</head> |
||||
<body> |
||||
|
||||
{{ template "navbar.html" .Navbar}} |
||||
|
||||
<main role="main"> |
||||
<div class="signup-form"> |
||||
<form action="/signup" method="post"> |
||||
<h2 class="text-center">Create New Account</h2> |
||||
|
||||
<div class="form-group"> |
||||
<span>Email:</span> |
||||
<input type="text" class="form-control" name="email" |
||||
placeholder="user@domain.com" required="required"> |
||||
</div> |
||||
|
||||
<div class="form-group"> |
||||
<span>Desired Username:</span> |
||||
<input type="text" class="form-control" name="desiredUsername" placeholder="Username" required="required"> |
||||
</div> |
||||
<div class="form-group"> |
||||
<p>Please use a unique password that you don't use anywhere |
||||
else. Minimum 12 characters.</p> |
||||
<span>New Password:</span> |
||||
<input type="password" class="form-control" |
||||
name="desiredPassword1" placeholder="Password" required="required"> |
||||
</div> |
||||
<div class="form-group"> |
||||
<span>New Password (again):</span> |
||||
<input type="password" class="form-control" |
||||
name="desiredPassword2" placeholder="Password |
||||
(again)" required="required"> |
||||
</div> |
||||
<div class="form-group"> |
||||
<button type="submit" class="btn btn-primary btn-block">Create |
||||
New Account</button> |
||||
</div> |
||||
<!-- <div class="clearfix"> |
||||
<label class="float-left form-check-label"><input type="checkbox"> Remember me</label> |
||||
<a href="#" class="float-right">Forgot Password?</a> |
||||
</div> |
||||
--> |
||||
</form> |
||||
<p class="text-center"><a href="#">Create an Account</a></p> |
||||
</div> |
||||
|
||||
|
||||
</main> |
||||
|
||||
{{ template "pagefooter.html" .PageFooter }} |
||||
</body> |
||||
</html> |
Loading…
Reference in new issue