From 75442d261d00c4032e2dd95a94fa6f73d07202b5 Mon Sep 17 00:00:00 2001 From: sneak Date: Mon, 28 Nov 2022 04:59:20 +0100 Subject: [PATCH] this is nowhere near working yet --- .gitignore | 1 + cmd/httpd/main.go | 25 +- go.mod | 5 + go.sum | 23 ++ internal/config/config.go | 83 ++++++ internal/database/database.go | 49 ++++ internal/globals/globals.go | 27 ++ .../{server => handlers}/handlehealthcheck.go | 4 +- internal/{server => handlers}/handleindex.go | 7 +- internal/{server => handlers}/handlelogin.go | 4 +- .../handlers.go => handlers/handlenow.go} | 4 +- internal/{server => handlers}/handlepanic.go | 4 +- internal/handlers/handlers.go | 61 ++++ internal/logger/logger.go | 99 +++++++ .../{server => middlewares}/middlewares.go | 10 +- internal/server/http.go | 20 +- internal/server/main.go | 269 ------------------ internal/server/routes.go | 2 +- internal/server/server.go | 178 ++++++++++++ 19 files changed, 567 insertions(+), 308 deletions(-) create mode 100644 internal/config/config.go create mode 100644 internal/database/database.go create mode 100644 internal/globals/globals.go rename internal/{server => handlers}/handlehealthcheck.go (90%) rename internal/{server => handlers}/handleindex.go (78%) rename internal/{server => handlers}/handlelogin.go (64%) rename internal/{server/handlers.go => handlers/handlenow.go} (75%) rename internal/{server => handlers}/handlepanic.go (76%) create mode 100644 internal/handlers/handlers.go create mode 100644 internal/logger/logger.go rename internal/{server => middlewares}/middlewares.go (91%) delete mode 100644 internal/server/main.go create mode 100644 internal/server/server.go diff --git a/.gitignore b/.gitignore index 09c78c5..3b432fe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /httpd debug.log /.env +/cmd/httpd/httpd diff --git a/cmd/httpd/main.go b/cmd/httpd/main.go index e6d0197..f9ed795 100644 --- a/cmd/httpd/main.go +++ b/cmd/httpd/main.go @@ -1,9 +1,13 @@ package main import ( - "os" - + "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/handlers" + "git.eeqj.de/sneak/gohttpserver/internal/logger" "git.eeqj.de/sneak/gohttpserver/internal/server" + "go.uber.org/fx" ) var ( @@ -13,5 +17,20 @@ var ( ) func main() { - os.Exit(server.Run(Appname, Version, Buildarch)) + globals.Appname = Appname + globals.Version = Version + globals.Buildarch = Buildarch + + fx.New( + fx.Provide( + config.New, + database.New, + globals.New, + handlers.New, + logger.New, + server.New, + ), + fx.Invoke(func(*server.Server) {}), + ).Run() + // os.Exit(server.Run(Appname, Version, Buildarch)) } diff --git a/go.mod b/go.mod index 0912a8a..25fc970 100644 --- a/go.mod +++ b/go.mod @@ -35,6 +35,11 @@ require ( github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.4.1 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/dig v1.15.0 // indirect + go.uber.org/fx v1.18.2 // indirect + go.uber.org/multierr v1.8.0 // indirect + go.uber.org/zap v1.21.0 // indirect golang.org/x/sys v0.2.0 // indirect golang.org/x/text v0.4.0 // indirect google.golang.org/protobuf v1.28.1 // indirect diff --git a/go.sum b/go.sum index dbed49f..32c4c3c 100644 --- a/go.sum +++ b/go.sum @@ -45,6 +45,7 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -279,12 +280,26 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/dig v1.15.0 h1:vq3YWr8zRj1eFGC7Gvf907hE0eRjPTZ1d3xHadD6liE= +go.uber.org/dig v1.15.0/go.mod h1:pKHs0wMynzL6brANhB2hLMro+zalv1osARTviTcqHLM= +go.uber.org/fx v1.18.2 h1:bUNI6oShr+OVFQeU8cDNbnN7VFsu+SsjHzUF51V/GAU= +go.uber.org/fx v1.18.2/go.mod h1:g0V1KMQ66zIRk8bLu3Ea5Jt2w/cHlOIp4wdRsgh0JaY= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= +go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= +go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -326,6 +341,7 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -359,6 +375,7 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= @@ -384,6 +401,7 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -423,8 +441,10 @@ golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -497,6 +517,7 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -605,10 +626,12 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..8418c75 --- /dev/null +++ b/internal/config/config.go @@ -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 +} diff --git a/internal/database/database.go b/internal/database/database.go new file mode 100644 index 0000000..be4b3e9 --- /dev/null +++ b/internal/database/database.go @@ -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 +} diff --git a/internal/globals/globals.go b/internal/globals/globals.go new file mode 100644 index 0000000..e2ed2c8 --- /dev/null +++ b/internal/globals/globals.go @@ -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 +} diff --git a/internal/server/handlehealthcheck.go b/internal/handlers/handlehealthcheck.go similarity index 90% rename from internal/server/handlehealthcheck.go rename to internal/handlers/handlehealthcheck.go index d9e8b33..479e340 100644 --- a/internal/server/handlehealthcheck.go +++ b/internal/handlers/handlehealthcheck.go @@ -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"` diff --git a/internal/server/handleindex.go b/internal/handlers/handleindex.go similarity index 78% rename from internal/server/handleindex.go rename to internal/handlers/handleindex.go index 7c54bd3..0c79b53 100644 --- a/internal/server/handleindex.go +++ b/internal/handlers/handleindex.go @@ -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) } } diff --git a/internal/server/handlelogin.go b/internal/handlers/handlelogin.go similarity index 64% rename from internal/server/handlelogin.go rename to internal/handlers/handlelogin.go index 57a5268..0dad604 100644 --- a/internal/server/handlelogin.go +++ b/internal/handlers/handlelogin.go @@ -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") } diff --git a/internal/server/handlers.go b/internal/handlers/handlenow.go similarity index 75% rename from internal/server/handlers.go rename to internal/handlers/handlenow.go index 8ab0e09..140b08b 100644 --- a/internal/server/handlers.go +++ b/internal/handlers/handlenow.go @@ -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"` } diff --git a/internal/server/handlepanic.go b/internal/handlers/handlepanic.go similarity index 76% rename from internal/server/handlepanic.go rename to internal/handlers/handlepanic.go index 77d8c29..385d1ca 100644 --- a/internal/server/handlepanic.go +++ b/internal/handlers/handlepanic.go @@ -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") } diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go new file mode 100644 index 0000000..7dcba34 --- /dev/null +++ b/internal/handlers/handlers.go @@ -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) +} diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 0000000..0bde904 --- /dev/null +++ b/internal/logger/logger.go @@ -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") +} diff --git a/internal/server/middlewares.go b/internal/middlewares/middlewares.go similarity index 91% rename from internal/server/middlewares.go rename to internal/middlewares/middlewares.go index 4877d55..4b35c96 100644 --- a/internal/server/middlewares.go +++ b/internal/middlewares/middlewares.go @@ -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{ diff --git a/internal/server/http.go b/internal/server/http.go index 9549330..7e814b1 100644 --- a/internal/server/http.go +++ b/internal/server/http.go @@ -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) } diff --git a/internal/server/main.go b/internal/server/main.go deleted file mode 100644 index 2681876..0000000 --- a/internal/server/main.go +++ /dev/null @@ -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") -} diff --git a/internal/server/routes.go b/internal/server/routes.go index dd8d7f8..27cb874 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -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 diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 0000000..217daf4 --- /dev/null +++ b/internal/server/server.go @@ -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()) + // } +}