this is nowhere near working yet
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
83
internal/config/config.go
Normal file
83
internal/config/config.go
Normal 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
|
||||
}
|
||||
49
internal/database/database.go
Normal file
49
internal/database/database.go
Normal 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
|
||||
}
|
||||
27
internal/globals/globals.go
Normal file
27
internal/globals/globals.go
Normal 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
|
||||
}
|
||||
@@ -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"`
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
61
internal/handlers/handlers.go
Normal file
61
internal/handlers/handlers.go
Normal 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
99
internal/logger/logger.go
Normal 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")
|
||||
}
|
||||
@@ -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{
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
178
internal/server/server.go
Normal 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())
|
||||
// }
|
||||
}
|
||||
Reference in New Issue
Block a user