diff --git a/.air.toml b/.air.toml index 8491d61..f9d6ad7 100644 --- a/.air.toml +++ b/.air.toml @@ -2,6 +2,9 @@ root = "." testdata_dir = "testdata" tmp_dir = "tmp" +[env] +file = ".env" + [build] args_bin = ["-log-level=DEBUG"] bin = "./tmp/main" diff --git a/.env.template b/.env.template new file mode 100644 index 0000000..8538c2e --- /dev/null +++ b/.env.template @@ -0,0 +1,4 @@ +export PULLEY_DATABASE_URI=postgres://pulley:passwd@localhost:5434/pulley +export PULLEY_PORT=5002 +export PULLEY_LOG_LEVEL="debug" + diff --git a/.gitignore b/.gitignore index 120a0ee..2f3b2bc 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ # Go workspace file go.work +# local environment setup tmp/ - .pg_data/ +.env diff --git a/.local.profile b/.local.profile deleted file mode 100644 index dc263d6..0000000 --- a/.local.profile +++ /dev/null @@ -1,5 +0,0 @@ -export PG_DATABASE=pulley -export PG_USER=pulley -export PG_PASSWD=passwd -export PG_URI=postgres://pulley:passwd@localhost:5434/pulley - diff --git a/cmd/api/handlers.go b/cmd/api/handlers.go index 2ec7ee2..c17b112 100644 --- a/cmd/api/handlers.go +++ b/cmd/api/handlers.go @@ -100,7 +100,7 @@ func (app *application) healthCheckHandler(w http.ResponseWriter, r *http.Reques env := envelope{ "status": "available", "system_info": map[string]string{ - "environment": app.config.env, + "environment": app.config.Env, "version": Version, }, } diff --git a/cmd/api/handlers_test.go b/cmd/api/handlers_test.go index 6050ad0..7a11338 100644 --- a/cmd/api/handlers_test.go +++ b/cmd/api/handlers_test.go @@ -12,10 +12,11 @@ import ( "testing" "git.runcible.io/learning/pulley/internal/assert" + "git.runcible.io/learning/pulley/internal/config" ) func newTestApplication() application { - cfg := config{env: "test"} + cfg := config.ServiceConfig{Env: "test"} return application{config: cfg, logger: slog.New(slog.NewTextHandler(io.Discard, nil))} } diff --git a/cmd/api/main.go b/cmd/api/main.go index f7cf781..9b8f581 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -3,7 +3,8 @@ package main import ( "context" "errors" - "flag" + + // "flag" "fmt" "io" "log/slog" @@ -13,40 +14,73 @@ import ( "syscall" "time" + "git.runcible.io/learning/pulley/internal/config" "git.runcible.io/learning/pulley/internal/logging" + "github.com/jackc/pgx/v5/pgxpool" ) const Version = "1.0.0" -type config struct { - port int - env string - logLevel string -} +//type config struct { +// port int +// env string +// logLevel string +//} type application struct { - config config + config config.ServiceConfig logger *slog.Logger } func run(ctx context.Context, w io.Writer, args []string) error { - var cfg config + // var cfg config + + // flagSet := flag.NewFlagSet(args[0], flag.ExitOnError) + + // flagSet.IntVar(&cfg.port, "port", 5002, "API server port") + // flagSet.StringVar(&cfg.env, "env", "development", "Environment (development|staging|production)") + // flagSet.StringVar(&cfg.logLevel, "log-level", "INFO", "Logging Level (INFO|DEBUG|WARN|ERROR)") + + // if err := flagSet.Parse(args[1:]); err != nil { + // return err + // } + + cfg := config.GetServiceConfig() + + logger := logging.InitLogging(cfg.LogLevel, w, true) + + signalCtx, stop := signal.NotifyContext(ctx, syscall.SIGTERM, syscall.SIGINT) + defer func() { + slog.Info("Calling signal watcher stop.") + stop() + }() - flagSet := flag.NewFlagSet(args[0], flag.ExitOnError) + // appCtx is a context that can be programatically called to initiate the shutdown process + // it inherits from signalCtx which will stop on receipt of an OS signal. You can think of + // appCtx as responding to internal signals in the app, and signalCtx as responding to + // external signals sent to the app. + appCtx, cancel := context.WithCancel(signalCtx) + defer cancel() - flagSet.IntVar(&cfg.port, "port", 5002, "API server port") - flagSet.StringVar(&cfg.env, "env", "development", "Environment (development|staging|production)") - flagSet.StringVar(&cfg.logLevel, "log-level", "INFO", "Logging Level (INFO|DEBUG|WARN|ERROR)") + pool, err := pgxpool.New(appCtx, cfg.DatabaseUri) + if err != nil { + slog.Error("Error encountered in creating pgx postgres connection pool", "error", err.Error()) + return err + } + defer func() { + slog.Info("Closing database connection pool") + pool.Close() + }() - if err := flagSet.Parse(args[1:]); err != nil { + if err := pool.Ping(appCtx); err != nil { + slog.Error("Error in attempting first postgres database connection") return err } - logger := logging.InitLogging(cfg.logLevel, w, true) app := application{config: cfg, logger: logger} srv := &http.Server{ - Addr: fmt.Sprintf("%s:%d", "0.0.0.0", app.config.port), + Addr: fmt.Sprintf("%s:%d", "0.0.0.0", app.config.Port), ErrorLog: slog.NewLogLogger(app.logger.Handler(), slog.LevelError), IdleTimeout: time.Minute, ReadTimeout: 5 * time.Second, @@ -70,12 +104,17 @@ func run(ctx context.Context, w io.Writer, args []string) error { }() // Handle graceful shutdown - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + // This was an older way to do it before 1.16. Now we can use signal.NotifyContext + // sigChan := make(chan os.Signal, 1) + // signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) // Block until signal is received - <-sigChan + //<-sigChan + + <-appCtx.Done() - shutdownCtx, shutdownRelease := context.WithTimeout(ctx, 10*time.Second) + // We don't want to inherit from appCtx because it has already been canceled at this point + shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second) defer shutdownRelease() if err := srv.Shutdown(shutdownCtx); err != nil { diff --git a/go.mod b/go.mod index 4e1eb6a..0ada839 100644 --- a/go.mod +++ b/go.mod @@ -3,3 +3,14 @@ module git.runcible.io/learning/pulley go 1.24.1 require github.com/julienschmidt/httprouter v1.3.0 + +require ( + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.7.5 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/kelseyhightower/envconfig v1.4.0 // indirect + golang.org/x/crypto v0.37.0 // indirect + golang.org/x/sync v0.13.0 // indirect + golang.org/x/text v0.24.0 // indirect +) diff --git a/go.sum b/go.sum index 096c54e..74f1c13 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,25 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs= +github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= +github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..17e5b33 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,16 @@ +package config + +import "github.com/kelseyhightower/envconfig" + +type ServiceConfig struct { + LogLevel string `default:"INFO"` + Port int `default:"5002"` + DatabaseUri string `envconfig:"DATABASE_URI" required:"true"` + Env string `default:"dev"` +} + +func GetServiceConfig() ServiceConfig { + var sc ServiceConfig + envconfig.MustProcess("PULLEY", &sc) + return sc +}