This commit is contained in:
commit
12fbb06666
20
.gitea/workflows/golangci-lint.yml
Normal file
20
.gitea/workflows/golangci-lint.yml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
name: golangci-lint
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
golangci:
|
||||||
|
name: lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: 'go.mod'
|
||||||
|
- name: golangci-lint
|
||||||
|
uses: golangci/golangci-lint-action@v6
|
||||||
|
with:
|
||||||
|
version: 'latest'
|
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Created by https://www.toptal.com/developers/gitignore/api/go
|
||||||
|
# Edit at https://www.toptal.com/developers/gitignore?templates=go
|
||||||
|
|
||||||
|
### Go ###
|
||||||
|
# If you prefer the allow list template instead of the deny list, see community template:
|
||||||
|
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||||
|
#
|
||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
# vendor/
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
||||||
|
|
||||||
|
# End of https://www.toptal.com/developers/gitignore/api/go
|
||||||
|
|
||||||
|
config.toml
|
||||||
|
server
|
9
assets/embed.go
Normal file
9
assets/embed.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package assets
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
//go:embed public
|
||||||
|
var Templates embed.FS
|
||||||
|
|
||||||
|
//go:embed static
|
||||||
|
var Static embed.FS
|
13
assets/public/html/footer.tmpl
Normal file
13
assets/public/html/footer.tmpl
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{{ define "footer" }}
|
||||||
|
<div class="footbar">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<footer class="footer full-width">
|
||||||
|
<p class="mtop">
|
||||||
|
<a href="#" target="_blank"></a>
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
26
assets/public/html/index.html
Normal file
26
assets/public/html/index.html
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<script src="./static/js/htmx.org@2.0.2.js"></script>
|
||||||
|
<!-- <link href="css/style.css" rel="stylesheet"> -->
|
||||||
|
<title>Hello, World!</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
{{ template "navbar" }}
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<p>Content</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ template "footer" }}
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
14
assets/public/html/navbar.tmpl
Normal file
14
assets/public/html/navbar.tmpl
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{{ define "navbar" }}
|
||||||
|
<div class="navbar">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="navigation">
|
||||||
|
<a href="/">Logo</a>
|
||||||
|
<ul class="navigation-items">
|
||||||
|
<li class="navigation-item"><a class="navigation-link" href="/">Home</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
28
assets/public/html/notFound.html
Normal file
28
assets/public/html/notFound.html
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<script src="./static/js/htmx.org@2.0.2.js"></script>
|
||||||
|
<!-- <link href="css/style.css" rel="stylesheet"> -->
|
||||||
|
<title>Hello, World!</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
{{ template "navbar" }}
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<h2>Not Found</h2>
|
||||||
|
<p>Page not Found</p>
|
||||||
|
<p>Request-ID: {{ .RequestID }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ template "footer" }}
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
1
assets/static/js/htmx.org@2.0.2.js
Normal file
1
assets/static/js/htmx.org@2.0.2.js
Normal file
File diff suppressed because one or more lines are too long
79
common/config.go
Normal file
79
common/config.go
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pelletier/go-toml/v2"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
l "github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ConfigPath = "./config.toml"
|
||||||
|
DefaultLogLevel = "info"
|
||||||
|
DefaultPort = 8080
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Log log
|
||||||
|
Http http
|
||||||
|
}
|
||||||
|
|
||||||
|
type http struct {
|
||||||
|
Port uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
type log struct {
|
||||||
|
Level string
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultConfig() Config {
|
||||||
|
return Config{
|
||||||
|
Log: log{
|
||||||
|
Level: DefaultLogLevel,
|
||||||
|
},
|
||||||
|
Http: http{
|
||||||
|
Port: DefaultPort,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadConfig(path string) Config {
|
||||||
|
logger := l.Error().Str("path", path)
|
||||||
|
config := defaultConfig()
|
||||||
|
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
logger.Err(err).Msg("cannot open file, using default config")
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
dec := toml.NewDecoder(file)
|
||||||
|
err = dec.Decode(&config)
|
||||||
|
if err != nil {
|
||||||
|
logger.Err(err).Msg("cannot parse config, using default")
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfig() Config {
|
||||||
|
return loadConfig(ConfigPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) LogLevel() zerolog.Level {
|
||||||
|
switch strings.ToLower(c.Log.Level) {
|
||||||
|
case "error":
|
||||||
|
return zerolog.ErrorLevel
|
||||||
|
case "debug":
|
||||||
|
return zerolog.DebugLevel
|
||||||
|
case "trace":
|
||||||
|
return zerolog.TraceLevel
|
||||||
|
case "warn":
|
||||||
|
return zerolog.WarnLevel
|
||||||
|
default:
|
||||||
|
return zerolog.InfoLevel
|
||||||
|
}
|
||||||
|
}
|
4
example.config.toml
Normal file
4
example.config.toml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
[http]
|
||||||
|
port = 8080
|
||||||
|
[log]
|
||||||
|
level = "info"
|
14
go.mod
Normal file
14
go.mod
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
module git.vbrandl.net/vbrandl/go-web-template
|
||||||
|
|
||||||
|
go 1.22.6
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gorilla/mux v1.8.1 // indirect
|
||||||
|
github.com/justinas/alice v1.2.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||||
|
github.com/rs/xid v1.5.0 // indirect
|
||||||
|
github.com/rs/zerolog v1.33.0 // indirect
|
||||||
|
golang.org/x/sys v0.12.0 // indirect
|
||||||
|
)
|
22
go.sum
Normal file
22
go.sum
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||||
|
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||||
|
github.com/justinas/alice v1.2.0 h1:+MHSA/vccVCF4Uq37S42jwlkvI2Xzl7zTPCN5BnZNVo=
|
||||||
|
github.com/justinas/alice v1.2.0/go.mod h1:fN5HRH/reO/zrUflLfTN43t3vXvKzvZIENsNEe7i7qA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||||
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
|
||||||
|
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
|
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||||
|
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
||||||
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
77
handlers/healthcheck.go
Normal file
77
handlers/healthcheck.go
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"runtime"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/hlog"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
jsonContentType = "application/json;charset=utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Timing struct {
|
||||||
|
TimeMillis time.Duration `json:"timeMillis"`
|
||||||
|
Source string `json:"source"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HealthCheckResult struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Messages []string `json:"messages"`
|
||||||
|
Time time.Time `json:"time"`
|
||||||
|
Timing []Timing `json:"timing"`
|
||||||
|
Response HealthCheckResponse `json:"response"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HealthCheckResponse struct {
|
||||||
|
IpAddress string `json:"ipAddress"`
|
||||||
|
MemUsage string `json:"memUsage"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) HealthCheck(w http.ResponseWriter, r *http.Request) {
|
||||||
|
l := hlog.FromRequest(r)
|
||||||
|
l.Info().Msg("HealthCheck")
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
t, _ := json.Marshal(HealthCheckResult{
|
||||||
|
Success: true,
|
||||||
|
Messages: []string{},
|
||||||
|
Time: time.Now().UTC(),
|
||||||
|
Timing: []Timing{
|
||||||
|
{
|
||||||
|
Source: "HealthCheck",
|
||||||
|
TimeMillis: time.Since(start),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Response: HealthCheckResponse{
|
||||||
|
IpAddress: getIP(r),
|
||||||
|
MemUsage: memUsage(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
w.Header().Set("content-type", jsonContentType)
|
||||||
|
_, _ = fmt.Fprint(w, string(t))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getIP(r *http.Request) string {
|
||||||
|
// forwarded := r.Header.Get("X-FORWARDED-FOR")
|
||||||
|
// if forwarded != "" {
|
||||||
|
// return forwarded
|
||||||
|
// }
|
||||||
|
return r.RemoteAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
func memUsage() string {
|
||||||
|
var m runtime.MemStats
|
||||||
|
runtime.ReadMemStats(&m)
|
||||||
|
result := fmt.Sprintf("memoryusage::Alloc = %v MB::TotalAlloc = %v MB::Sys = %v MB::tNumGC = %v", bToMb(m.Alloc), bToMb(m.TotalAlloc), bToMb(m.Sys), m.NumGC)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func bToMb(b uint64) uint64 {
|
||||||
|
return b / 1024 / 1024
|
||||||
|
}
|
22
handlers/index.go
Normal file
22
handlers/index.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/hlog"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (app *Application) Index(w http.ResponseWriter, r *http.Request) {
|
||||||
|
l := hlog.FromRequest(r)
|
||||||
|
l.Info().Msg("Index")
|
||||||
|
|
||||||
|
reqId := RequestID(r)
|
||||||
|
|
||||||
|
err := indexTemplate.Execute(w, templateData{
|
||||||
|
RequestID: reqId,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
l.Error().Err(err).Msg("error executing template")
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
48
handlers/index_test.go
Normal file
48
handlers/index_test.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package handlers_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.vbrandl.net/vbrandl/go-web-template/assets"
|
||||||
|
"git.vbrandl.net/vbrandl/go-web-template/common"
|
||||||
|
"git.vbrandl.net/vbrandl/go-web-template/handlers"
|
||||||
|
"git.vbrandl.net/vbrandl/go-web-template/service"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PrepareApplication() handlers.Application {
|
||||||
|
config := common.NewConfig()
|
||||||
|
ser := service.New(config)
|
||||||
|
zerolog.SetGlobalLevel(config.LogLevel())
|
||||||
|
|
||||||
|
app, err := handlers.NewApplication(ser, assets.Static)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("failed creating application")
|
||||||
|
}
|
||||||
|
|
||||||
|
return app
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIndex(t *testing.T) {
|
||||||
|
app := PrepareApplication()
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
app.Handler().ServeHTTP(w, req)
|
||||||
|
// app.Index(w, req)
|
||||||
|
res := w.Result()
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
if res.StatusCode != 200 {
|
||||||
|
t.Errorf("expected status to be 200, got %d", res.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
reqId := res.Header.Get("x-request-id")
|
||||||
|
if reqId == "" {
|
||||||
|
t.Errorf("expected `x-request-id` header to be present, got %s", reqId)
|
||||||
|
}
|
||||||
|
}
|
23
handlers/not_found.go
Normal file
23
handlers/not_found.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/hlog"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (app *Application) NotFound(w http.ResponseWriter, r *http.Request) {
|
||||||
|
l := hlog.FromRequest(r)
|
||||||
|
l.Info().Msg("Not found")
|
||||||
|
|
||||||
|
reqId := RequestID(r)
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
|
||||||
|
err := notFoundTemplate.Execute(w, templateData{
|
||||||
|
RequestID: reqId,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
l.Error().Err(err).Msg("error executing template")
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
37
handlers/not_found_test.go
Normal file
37
handlers/not_found_test.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package handlers_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNotFound(t *testing.T) {
|
||||||
|
app := PrepareApplication()
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/some/invalid/path", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
app.Handler().ServeHTTP(w, req)
|
||||||
|
res := w.Result()
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
if res.StatusCode != 404 {
|
||||||
|
t.Errorf("expected status to be 404, got %d", res.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
reqId := res.Header.Get("x-request-id")
|
||||||
|
if reqId == "" {
|
||||||
|
t.Errorf("expected `x-request-id` header to be present, got %s", reqId)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected error to be nil, got %v", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(data), reqId) {
|
||||||
|
t.Errorf("expected body to contain request ID but it does not")
|
||||||
|
}
|
||||||
|
}
|
79
handlers/route.go
Normal file
79
handlers/route.go
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.vbrandl.net/vbrandl/go-web-template/service"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/justinas/alice"
|
||||||
|
"github.com/rs/zerolog/hlog"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Application struct {
|
||||||
|
Service *service.Service
|
||||||
|
StaticFiles fs.FS
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewApplication(service *service.Service, staticFiles fs.FS) (Application, error) {
|
||||||
|
application := Application{
|
||||||
|
Service: service,
|
||||||
|
StaticFiles: staticFiles,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := application.ParseTemplates()
|
||||||
|
|
||||||
|
return application, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) Routes() *mux.Router {
|
||||||
|
router := mux.NewRouter().StrictSlash(true)
|
||||||
|
|
||||||
|
router.HandleFunc("/", app.Index).Methods("GET")
|
||||||
|
router.HandleFunc("/health", app.HealthCheck).Methods("GET")
|
||||||
|
|
||||||
|
// strip off the location such that we route correctly
|
||||||
|
staticContent, err := fs.Sub(app.StaticFiles, "static")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("error with static content")
|
||||||
|
}
|
||||||
|
f := http.FileServerFS(staticContent)
|
||||||
|
router.PathPrefix("/static/").Handler(http.StripPrefix("/static", f))
|
||||||
|
|
||||||
|
router.NotFoundHandler = http.HandlerFunc(app.NotFound)
|
||||||
|
|
||||||
|
return router
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) Handler() http.Handler {
|
||||||
|
logger := log.Logger
|
||||||
|
middlewares := alice.New().
|
||||||
|
Append(hlog.NewHandler(logger)).
|
||||||
|
Append(hlog.AccessHandler(func(r *http.Request, status int, size int, duration time.Duration) {
|
||||||
|
hlog.FromRequest(r).Info().
|
||||||
|
Str("method", r.Method).
|
||||||
|
Stringer("url", r.URL).
|
||||||
|
Int("status", status).
|
||||||
|
Int("size", size).
|
||||||
|
Str("remote-addr", r.RemoteAddr).
|
||||||
|
Dur("duration", duration).
|
||||||
|
Str("user-agent", r.UserAgent()).
|
||||||
|
Msg("")
|
||||||
|
})).
|
||||||
|
Append(hlog.RequestIDHandler("request-id", "x-request-id"))
|
||||||
|
|
||||||
|
routes := app.Routes()
|
||||||
|
return middlewares.Then(routes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RequestID(r *http.Request) string {
|
||||||
|
if id, ok := hlog.IDFromRequest(r); ok {
|
||||||
|
return id.String()
|
||||||
|
} else {
|
||||||
|
l := hlog.FromRequest(r)
|
||||||
|
l.Warn().Msg("No associated request ID")
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
}
|
30
handlers/template.go
Normal file
30
handlers/template.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
|
||||||
|
"git.vbrandl.net/vbrandl/go-web-template/assets"
|
||||||
|
)
|
||||||
|
|
||||||
|
var indexTemplate *template.Template
|
||||||
|
var notFoundTemplate *template.Template
|
||||||
|
|
||||||
|
type templateData struct {
|
||||||
|
RequestID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) ParseTemplates() error {
|
||||||
|
t, err := template.ParseFS(assets.Templates, "public/html/index.html", "public/html/navbar.tmpl", "public/html/footer.tmpl")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
indexTemplate = t
|
||||||
|
|
||||||
|
t, err = template.ParseFS(assets.Templates, "public/html/notFound.html", "public/html/navbar.tmpl", "public/html/footer.tmpl")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
notFoundTemplate = t
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
36
service/service.go
Normal file
36
service/service.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.vbrandl.net/vbrandl/go-web-template/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
BackgroundJobsStarted bool
|
||||||
|
ServiceMutex sync.Mutex
|
||||||
|
StartTime time.Time
|
||||||
|
Config common.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(config common.Config) *Service {
|
||||||
|
ser := &Service{
|
||||||
|
BackgroundJobsStarted: false,
|
||||||
|
ServiceMutex: sync.Mutex{},
|
||||||
|
StartTime: time.Now(),
|
||||||
|
Config: config,
|
||||||
|
}
|
||||||
|
|
||||||
|
ser.StartBackground()
|
||||||
|
return ser
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ser *Service) StartBackground() {
|
||||||
|
ser.ServiceMutex.Lock()
|
||||||
|
defer ser.ServiceMutex.Unlock()
|
||||||
|
|
||||||
|
ser.BackgroundJobsStarted = true
|
||||||
|
|
||||||
|
// TODO: start jobs
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user