From dd778174a71c4248ae2970b1ac3bf52b4c6ab73d Mon Sep 17 00:00:00 2001 From: Jeffrey Paul Date: Sun, 29 Jan 2023 03:06:05 +0000 Subject: [PATCH] sneak/integrate-di (#17) moving this to use uber/fx di framework instead of the ad hoc di setup before Co-authored-by: sneak Reviewed-on: https://git.eeqj.de/sneak/gohttpserver/pulls/17 --- .gitignore | 1 + cmd/httpd/main.go | 29 +- go.mod | 8 + go.sum | 28 ++ internal/config/config.go | 90 ++++++ internal/database/database.go | 52 ++++ internal/globals/globals.go | 27 ++ internal/handlers/handlers.go | 57 ++++ internal/handlers/healthcheck.go | 12 + internal/handlers/index.go | 19 ++ internal/handlers/login.go | 19 ++ .../{server/handlers.go => handlers/now.go} | 4 +- .../handlepanic.go => handlers/panic.go} | 4 +- internal/healthcheck/healthcheck.go | 71 +++++ internal/logger/logger.go | 97 +++++++ .../middleware.go} | 36 ++- internal/server/handlehealthcheck.go | 30 -- internal/server/handleindex.go | 21 -- internal/server/handlelogin.go | 12 - internal/server/http.go | 24 +- internal/server/main.go | 269 ------------------ internal/server/routes.go | 31 +- internal/server/server.go | 178 ++++++++++++ templates/htmlfooter.html | 3 + templates/htmlheader.html | 4 + templates/index.html | 89 +----- templates/login.html | 80 +----- templates/navbar.html | 64 +++++ templates/pagefooter.html | 3 + templates/signup.html | 76 +++++ templates/templates.go | 14 +- 31 files changed, 924 insertions(+), 528 deletions(-) create mode 100644 internal/config/config.go create mode 100644 internal/database/database.go create mode 100644 internal/globals/globals.go create mode 100644 internal/handlers/handlers.go create mode 100644 internal/handlers/healthcheck.go create mode 100644 internal/handlers/index.go create mode 100644 internal/handlers/login.go rename internal/{server/handlers.go => handlers/now.go} (75%) rename internal/{server/handlepanic.go => handlers/panic.go} (76%) create mode 100644 internal/healthcheck/healthcheck.go create mode 100644 internal/logger/logger.go rename internal/{server/middlewares.go => middleware/middleware.go} (78%) delete mode 100644 internal/server/handlehealthcheck.go delete mode 100644 internal/server/handleindex.go delete mode 100644 internal/server/handlelogin.go delete mode 100644 internal/server/main.go create mode 100644 internal/server/server.go create mode 100644 templates/htmlfooter.html create mode 100644 templates/htmlheader.html create mode 100644 templates/navbar.html create mode 100644 templates/pagefooter.html create mode 100644 templates/signup.html 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..c105754 100644 --- a/cmd/httpd/main.go +++ b/cmd/httpd/main.go @@ -1,9 +1,15 @@ 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/healthcheck" + "git.eeqj.de/sneak/gohttpserver/internal/logger" + "git.eeqj.de/sneak/gohttpserver/internal/middleware" "git.eeqj.de/sneak/gohttpserver/internal/server" + "go.uber.org/fx" ) var ( @@ -13,5 +19,22 @@ 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, + middleware.New, + healthcheck.New, + ), + fx.Invoke(func(*server.Server) {}), + ).Run() + // os.Exit(server.Run(Appname, Version, Buildarch)) } diff --git a/go.mod b/go.mod index 0912a8a..cb4c74e 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,8 @@ require ( github.com/rs/zerolog v1.28.0 github.com/slok/go-http-metrics v0.10.0 github.com/spf13/viper v1.14.0 + go.uber.org/fx v1.18.2 + google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e ) require ( @@ -35,8 +37,14 @@ 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/multierr v1.8.0 // indirect + go.uber.org/zap v1.21.0 // indirect + golang.org/x/net v0.0.0-20221014081412-f15817d10f9b // indirect golang.org/x/sys v0.2.0 // indirect golang.org/x/text v0.4.0 // indirect + google.golang.org/grpc v1.50.1 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index dbed49f..dd7b16b 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,10 +375,12 @@ 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= golang.org/x/net v0.0.0-20221014081412-f15817d10f9b h1:tvrvnPFcdzp294diPnrdZZZ8XUt2Tyj7svb7X52iDuU= +golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -384,6 +402,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 +442,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 +518,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= @@ -563,6 +585,8 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e h1:S9GbmC1iCgvbLyAokVCwiO6tVIrU9Y7c5oMx1V/ki/Y= +google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -579,6 +603,8 @@ google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.50.1 h1:DS/BukOZWp8s6p4Dt/tOaJaTQyPyOoCcrjroHuCeLzY= +google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -605,10 +631,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..76481ac --- /dev/null +++ b/internal/config/config.go @@ -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 +} diff --git a/internal/database/database.go b/internal/database/database.go new file mode 100644 index 0000000..a996c3a --- /dev/null +++ b/internal/database/database.go @@ -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 +} 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/handlers/handlers.go b/internal/handlers/handlers.go new file mode 100644 index 0000000..e9d652f --- /dev/null +++ b/internal/handlers/handlers.go @@ -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) +} diff --git a/internal/handlers/healthcheck.go b/internal/handlers/healthcheck.go new file mode 100644 index 0000000..34fed69 --- /dev/null +++ b/internal/handlers/healthcheck.go @@ -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) + } +} diff --git a/internal/handlers/index.go b/internal/handlers/index.go new file mode 100644 index 0000000..7895d49 --- /dev/null +++ b/internal/handlers/index.go @@ -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) + } + } +} diff --git a/internal/handlers/login.go b/internal/handlers/login.go new file mode 100644 index 0000000..f15b847 --- /dev/null +++ b/internal/handlers/login.go @@ -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) + } + } +} diff --git a/internal/server/handlers.go b/internal/handlers/now.go similarity index 75% rename from internal/server/handlers.go rename to internal/handlers/now.go index 8ab0e09..140b08b 100644 --- a/internal/server/handlers.go +++ b/internal/handlers/now.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/panic.go similarity index 76% rename from internal/server/handlepanic.go rename to internal/handlers/panic.go index 77d8c29..385d1ca 100644 --- a/internal/server/handlepanic.go +++ b/internal/handlers/panic.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/healthcheck/healthcheck.go b/internal/healthcheck/healthcheck.go new file mode 100644 index 0000000..c7ff3a9 --- /dev/null +++ b/internal/healthcheck/healthcheck.go @@ -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 +} diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 0000000..c127ba2 --- /dev/null +++ b/internal/logger/logger.go @@ -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") +} diff --git a/internal/server/middlewares.go b/internal/middleware/middleware.go similarity index 78% rename from internal/server/middlewares.go rename to internal/middleware/middleware.go index 4877d55..a6605ed 100644 --- a/internal/server/middlewares.go +++ b/internal/middleware/middleware.go @@ -1,19 +1,43 @@ -package server +package middleware import ( "net" "net/http" "time" + "git.eeqj.de/sneak/gohttpserver/internal/config" + "git.eeqj.de/sneak/gohttpserver/internal/globals" + "git.eeqj.de/sneak/gohttpserver/internal/logger" basicauth "github.com/99designs/basicauth-go" "github.com/go-chi/chi/middleware" "github.com/go-chi/cors" + "github.com/rs/zerolog" metrics "github.com/slok/go-http-metrics/metrics/prometheus" ghmm "github.com/slok/go-http-metrics/middleware" "github.com/slok/go-http-metrics/middleware/std" "github.com/spf13/viper" + "go.uber.org/fx" ) +type MiddlewareParams struct { + fx.In + Logger *logger.Logger + Globals *globals.Globals + Config *config.Config +} + +type Middleware struct { + log *zerolog.Logger + params *MiddlewareParams +} + +func New(lc fx.Lifecycle, params MiddlewareParams) (*Middleware, error) { + s := new(Middleware) + s.params = ¶ms + s.log = params.Logger.Get() + return s, nil +} + // the following is from // https://learning-cloud-native-go.github.io/docs/a6.adding_zerolog_logger/ @@ -45,7 +69,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 *Middleware) Logging() 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 +97,7 @@ func (s *server) LoggingMiddleware() func(http.Handler) http.Handler { } } -func (s *server) CORSMiddleware() func(http.Handler) http.Handler { +func (s *Middleware) CORS() 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 +112,7 @@ func (s *server) CORSMiddleware() func(http.Handler) http.Handler { }) } -func (s *server) AuthMiddleware() func(http.Handler) http.Handler { +func (s *Middleware) Auth() 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 +122,7 @@ func (s *server) AuthMiddleware() func(http.Handler) http.Handler { } } -func (s *server) MetricsMiddleware() func(http.Handler) http.Handler { +func (s *Middleware) Metrics() func(http.Handler) http.Handler { mdlw := ghmm.New(ghmm.Config{ Recorder: metrics.NewRecorder(metrics.Config{}), }) @@ -107,7 +131,7 @@ func (s *server) MetricsMiddleware() func(http.Handler) http.Handler { } } -func (s *server) MetricsAuthMiddleware() func(http.Handler) http.Handler { +func (s *Middleware) MetricsAuth() func(http.Handler) http.Handler { return basicauth.New( "metrics", map[string][]string{ diff --git a/internal/server/handlehealthcheck.go b/internal/server/handlehealthcheck.go deleted file mode 100644 index d9e8b33..0000000 --- a/internal/server/handlehealthcheck.go +++ /dev/null @@ -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) - } -} diff --git a/internal/server/handleindex.go b/internal/server/handleindex.go deleted file mode 100644 index 7c54bd3..0000000 --- a/internal/server/handleindex.go +++ /dev/null @@ -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) - } - } -} diff --git a/internal/server/handlelogin.go b/internal/server/handlelogin.go deleted file mode 100644 index 57a5268..0000000 --- a/internal/server/handlelogin.go +++ /dev/null @@ -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") - } -} diff --git a/internal/server/http.go b/internal/server/http.go index 9549330..6c25ffc 100644 --- a/internal/server/http.go +++ b/internal/server/http.go @@ -1,14 +1,13 @@ package server import ( - "encoding/json" "fmt" "net/http" "time" ) -func (s *server) serveUntilShutdown() { - listenAddr := fmt.Sprintf(":%d", s.port) +func (s *Server) serveUntilShutdown() { + listenAddr := fmt.Sprintf(":%d", s.params.Config.Port) s.httpServer = &http.Server{ Addr: listenAddr, ReadTimeout: 10 * time.Second, @@ -19,7 +18,7 @@ func (s *server) serveUntilShutdown() { // add routes // this does any necessary setup in each handler - s.routes() + s.SetupRoutes() s.log.Info().Str("listenaddr", listenAddr).Msg("http begin listen") if err := s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { @@ -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..bfdffed 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 @@ -23,16 +23,16 @@ func (s *server) routes() { s.router.Use(middleware.Recoverer) s.router.Use(middleware.RequestID) - s.router.Use(s.LoggingMiddleware()) + s.router.Use(s.mw.Logging()) // add metrics middleware only if we can serve them behind auth if viper.GetString("METRICS_USERNAME") != "" { - s.router.Use(s.MetricsMiddleware()) + s.router.Use(s.mw.Metrics()) } // set up CORS headers. you'll probably want to configure that // in middlewares.go. - s.router.Use(s.CORSMiddleware()) + s.router.Use(s.mw.CORS()) // CHANGEME to suit your needs, or pull from config. // timeout for request context; your handlers must finish within @@ -57,39 +57,48 @@ func (s *server) routes() { // complete docs: https://github.com/go-chi/chi //////////////////////////////////////////////////////////////////////// - s.router.Get("/", s.handleIndex()) + s.router.Get("/", s.h.HandleIndex()) s.router.Mount("/s", http.StripPrefix("/s", http.FileServer(http.FS(static.Static)))) s.router.Route("/api/v1", func(r chi.Router) { - r.Get("/now", s.handleNow()) + r.Get("/now", s.h.HandleNow()) }) // if you want to use a general purpose middleware (http.Handler // wrapper) on a specific HandleFunc route, you need to take the // .ServeHTTP of the http.Handler to get its HandleFunc, viz: - authMiddleware := s.AuthMiddleware() + auth := s.mw.Auth() s.router.Get( "/login", - authMiddleware(s.handleLogin()).ServeHTTP, + auth(s.h.HandleLoginGET()).ServeHTTP, ) + s.router.Get( + "/signup", + auth(s.h.HandleSignupGET()).ServeHTTP, + ) + + s.router.Post( + "/signup", + auth(s.h.HandleSignupPOST()).ServeHTTP, + ) // route that panics for testing // CHANGEME remove this s.router.Get( "/panic", - s.handlePanic(), + s.h.HandlePanic(), ) s.router.Get( "/.well-known/healthcheck.json", - s.handleHealthCheck(), + s.h.HandleHealthCheck(), ) // set up authenticated /metrics route: if viper.GetString("METRICS_USERNAME") != "" { s.router.Group(func(r chi.Router) { - r.Use(s.MetricsAuthMiddleware()) + r.Use(s.mw.MetricsAuth()) r.Get("/metrics", http.HandlerFunc(promhttp.Handler().ServeHTTP)) }) } diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 0000000..64e0ba6 --- /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/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()) + // } +} diff --git a/templates/htmlfooter.html b/templates/htmlfooter.html new file mode 100644 index 0000000..2a60c59 --- /dev/null +++ b/templates/htmlfooter.html @@ -0,0 +1,3 @@ + + + diff --git a/templates/htmlheader.html b/templates/htmlheader.html new file mode 100644 index 0000000..2d3e82d --- /dev/null +++ b/templates/htmlheader.html @@ -0,0 +1,4 @@ + + {{ .HTMLTitle }} + + diff --git a/templates/index.html b/templates/index.html index 426c8ac..b038f44 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,84 +1,29 @@ - - Changeme: Go HTTP Server Boilerplate - - + {{ template "htmlheader.html" . }} - - + {{ template "navbar.html" .}}

Hello, world!

+

gohttpserver

This is a boilerplate application for you to use as a base for your own sites and services.

+

+ Find more info at https://git.eeqj.de/sneak/gohttpserver. +

+

+ This software is provided by @sneak + and is released unconditionally into the public domain. +

Learn more »

-
-

© No rights reserved - This is in the public domain!

-
- - - + {{ template "pagefooter.html" . }} + {{ template "htmlfooter.html" . }} diff --git a/templates/login.html b/templates/login.html index b8e654b..1b356c4 100644 --- a/templates/login.html +++ b/templates/login.html @@ -1,10 +1,7 @@ - - Changeme: Go HTTP Server Boilerplate - - + {{ template "htmlheader.html" . }} + + + + {{ template "navbar.html" .Navbar}} + +
+ + + +
+ + {{ template "pagefooter.html" .PageFooter }} + + diff --git a/templates/templates.go b/templates/templates.go index 43e93ee..0184d6a 100644 --- a/templates/templates.go +++ b/templates/templates.go @@ -2,12 +2,21 @@ package templates import ( "embed" - "strings" + "text/template" ) //go:embed *.html -var Templates embed.FS +var TemplatesRaw embed.FS +var TemplatesParsed *template.Template +func GetParsed() *template.Template { + if TemplatesParsed == nil { + TemplatesParsed = template.Must(template.ParseFS(TemplatesRaw, "*")) + } + return TemplatesParsed +} + +/* func MustString(filename string) string { bytes, error := Templates.ReadFile(filename) if error != nil { @@ -17,3 +26,4 @@ func MustString(filename string) string { out.Write(bytes) return out.String() } +*/