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!
+
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 »
-
-
-
-
+ {{ 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()
}
+*/