Gabe Farrell 9 months ago
commit 7262164ef0

@ -0,0 +1,67 @@
package app
import (
"context"
"fmt"
"io"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"git.mnrva.dev/gabehf/go-project-template/app/middleware"
"git.mnrva.dev/gabehf/go-project-template/internal/cfg"
"git.mnrva.dev/gabehf/go-project-template/internal/db"
"git.mnrva.dev/gabehf/go-project-template/internal/logger"
"github.com/go-chi/chi/v5"
chimiddleware "github.com/go-chi/chi/v5/middleware"
"github.com/rs/zerolog"
)
func NewServer(logger *zerolog.Logger, cfg *cfg.Config, db db.DB) http.Handler {
mux := chi.NewRouter()
// bind general middleware to mux
mux.Use(middleware.Logger(logger))
mux.Use(chimiddleware.RealIP)
mux.Use(chimiddleware.Recoverer)
// call router binds on mux
bindRoutes(mux, logger, cfg, db)
return mux
}
func Run(
ctx context.Context,
getenv func(string) string,
stdout io.Writer,
db db.DB,
) error {
cfg, err := cfg.Load(getenv)
if err != nil {
return fmt.Errorf("failed to load configuration: %v", err)
}
logger := logger.Get(cfg).Output(stdout)
srv := NewServer(&logger, cfg, db)
httpServer := &http.Server{
Addr: cfg.ListenAddr,
Handler: srv,
}
logger.Info().Msg("listening on " + cfg.ListenAddr)
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
return err
}
// Wait for interrupt signal to gracefully shutdown the server with a timeout of 10 seconds.
// Use a buffered channel to avoid missing signals as recommended for signal.Notify
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
<-quit
logger.Info().Msg("recieved server shutdown notice")
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
logger.Info().Msg("waiting for all processes to finish...")
if err := httpServer.Shutdown(ctx); err != nil {
return err
}
logger.Info().Msg("shutdown successful")
return nil
}

@ -0,0 +1,2 @@
// package handlers implements route handlers
package handlers

@ -0,0 +1,49 @@
package middleware
import (
"net/http"
"runtime/debug"
"time"
"github.com/go-chi/chi/v5/middleware"
"github.com/rs/zerolog"
)
func Logger(logger *zerolog.Logger) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
log := logger.With().Logger()
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
t1 := time.Now()
defer func() {
t2 := time.Now()
if rec := recover(); rec != nil {
log.Error().
Str("type", "error").
Timestamp().
Interface("recover_info", rec).
Bytes("debug_stack", debug.Stack()).
Msg("log system error")
http.Error(ww, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
log.Info().
Str("type", "access").
Timestamp().
Fields(map[string]interface{}{
"remote_ip": r.RemoteAddr,
"url": r.URL.Path,
"proto": r.Proto,
"method": r.Method,
"user_agent": r.Header.Get("User-Agent"),
"status": ww.Status(),
"latency_ms": float64(t2.Sub(t1).Nanoseconds()) / 1000000.0,
"bytes_in": r.Header.Get("Content-Length"),
"bytes_out": ww.BytesWritten(),
}).
Msg("incoming_request")
}()
next.ServeHTTP(ww, r)
}
return http.HandlerFunc(fn)
}
}

@ -0,0 +1,14 @@
package app
import (
"git.mnrva.dev/gabehf/go-project-template/internal/cfg"
"git.mnrva.dev/gabehf/go-project-template/internal/db"
"github.com/go-chi/chi/v5"
"github.com/rs/zerolog"
)
func bindRoutes(r *chi.Mux, logger *zerolog.Logger, cfg *cfg.Config, db db.DB) {
r.Route("/api/v1", func(r chi.Router) {
// define routes here
})
}

@ -0,0 +1,21 @@
package main
import (
"context"
"fmt"
"os"
"git.mnrva.dev/gabehf/go-project-template/app"
"git.mnrva.dev/gabehf/go-project-template/internal/cfg"
"git.mnrva.dev/gabehf/go-project-template/internal/db/memorydb"
)
func main() {
ctx := context.Background()
store := memorydb.New(&cfg.Config{})
defer store.Close()
if err := app.Run(ctx, os.Getenv, os.Stdout, store); err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(1)
}
}

@ -0,0 +1,14 @@
module git.mnrva.dev/gabehf/go-project-template
go 1.23.7
require (
github.com/go-chi/chi/v5 v5.2.1
github.com/rs/zerolog v1.33.0
)
require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
golang.org/x/sys v0.12.0 // indirect
)

@ -0,0 +1,17 @@
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
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/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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=

@ -0,0 +1,28 @@
package cfg
const (
BASE_URL_ENV = "go-project-template_BASE_URL"
DATABASE_URL_ENV = "go-project-template_DATABASE_URL"
LISTEN_ADDR_ENV = "go-project-template_LISTEN_ADDR"
)
type Config struct {
ListenAddr string
BaseUrl string
DatabaseUrl string
LogLevel int
}
func Load(getenv func(string) string) (*Config, error) {
cfg := new(Config)
cfg.BaseUrl = getenv(BASE_URL_ENV)
if cfg.BaseUrl == "" {
cfg.BaseUrl = "http://localhost"
}
cfg.DatabaseUrl = getenv(DATABASE_URL_ENV)
cfg.ListenAddr = getenv(LISTEN_ADDR_ENV)
if cfg.ListenAddr == "" {
cfg.ListenAddr = ":3000"
}
return cfg, nil
}

@ -0,0 +1,6 @@
// package db defines the database interface
package db
type DB interface {
Close()
}

@ -0,0 +1,20 @@
// memorydb implements the db interface with an in-memory db for testing
package memorydb
import (
"sync"
"git.mnrva.dev/gabehf/go-project-template/internal/cfg"
)
type MemoryDB struct {
m map[string]interface{}
mut sync.RWMutex
}
func New(cfg *cfg.Config) *MemoryDB {
return new(MemoryDB)
}
func (d *MemoryDB) Close() {
}

@ -0,0 +1,29 @@
package logger
import (
"os"
"sync"
"git.mnrva.dev/gabehf/go-project-template/internal/cfg"
"github.com/rs/zerolog"
)
var once sync.Once
var log zerolog.Logger
func Get(cfg *cfg.Config) zerolog.Logger {
once.Do(func() {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs
logLevel := cfg.LogLevel
log = zerolog.New(os.Stdout).
Level(zerolog.Level(logLevel)).
With().
Timestamp().
Logger()
})
return log
}

@ -0,0 +1,2 @@
// package models defines the schema of objects in the database
package models
Loading…
Cancel
Save