From 7262164ef0ea8c475c260af9d4b1c3480f30ca40 Mon Sep 17 00:00:00 2001 From: Gabe Farrell Date: Sun, 16 Mar 2025 10:51:47 -0400 Subject: [PATCH] first --- app/engine.go | 67 +++++++++++++++++++++++++++++++ app/handlers/handlers.go | 2 + app/middleware/logger.go | 49 ++++++++++++++++++++++ app/routes.go | 14 +++++++ cmd/api/main.go | 21 ++++++++++ go.mod | 14 +++++++ go.sum | 17 ++++++++ internal/cfg/cfg.go | 28 +++++++++++++ internal/db/db.go | 6 +++ internal/db/memorydb/memory_db.go | 20 +++++++++ internal/logger/logger.go | 29 +++++++++++++ internal/models/models.go | 2 + 12 files changed, 269 insertions(+) create mode 100644 app/engine.go create mode 100644 app/handlers/handlers.go create mode 100644 app/middleware/logger.go create mode 100644 app/routes.go create mode 100644 cmd/api/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/cfg/cfg.go create mode 100644 internal/db/db.go create mode 100644 internal/db/memorydb/memory_db.go create mode 100644 internal/logger/logger.go create mode 100644 internal/models/models.go diff --git a/app/engine.go b/app/engine.go new file mode 100644 index 0000000..c918824 --- /dev/null +++ b/app/engine.go @@ -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 +} diff --git a/app/handlers/handlers.go b/app/handlers/handlers.go new file mode 100644 index 0000000..4ab8cd3 --- /dev/null +++ b/app/handlers/handlers.go @@ -0,0 +1,2 @@ +// package handlers implements route handlers +package handlers diff --git a/app/middleware/logger.go b/app/middleware/logger.go new file mode 100644 index 0000000..691d70b --- /dev/null +++ b/app/middleware/logger.go @@ -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) + } +} diff --git a/app/routes.go b/app/routes.go new file mode 100644 index 0000000..a397e17 --- /dev/null +++ b/app/routes.go @@ -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 + }) +} diff --git a/cmd/api/main.go b/cmd/api/main.go new file mode 100644 index 0000000..5a4ae62 --- /dev/null +++ b/cmd/api/main.go @@ -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) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..967fe67 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7d3666d --- /dev/null +++ b/go.sum @@ -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= diff --git a/internal/cfg/cfg.go b/internal/cfg/cfg.go new file mode 100644 index 0000000..d4062ac --- /dev/null +++ b/internal/cfg/cfg.go @@ -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 +} diff --git a/internal/db/db.go b/internal/db/db.go new file mode 100644 index 0000000..75d5868 --- /dev/null +++ b/internal/db/db.go @@ -0,0 +1,6 @@ +// package db defines the database interface +package db + +type DB interface { + Close() +} diff --git a/internal/db/memorydb/memory_db.go b/internal/db/memorydb/memory_db.go new file mode 100644 index 0000000..a1a7016 --- /dev/null +++ b/internal/db/memorydb/memory_db.go @@ -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() { +} diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 0000000..ee74613 --- /dev/null +++ b/internal/logger/logger.go @@ -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 +} diff --git a/internal/models/models.go b/internal/models/models.go new file mode 100644 index 0000000..3ef9ed9 --- /dev/null +++ b/internal/models/models.go @@ -0,0 +1,2 @@ +// package models defines the schema of objects in the database +package models