Adding:
continuous-integration/drone/push Build is passing Details

- envconfig for configuration
- .env file integration with air
- pgx + pgxpool for database connection
- switched to using signal.NotifyContext for graceful shutdown
main
Drew Bednar 2 weeks ago
parent 47b2e1f6d2
commit f9a21f475a

@ -2,6 +2,9 @@ root = "."
testdata_dir = "testdata" testdata_dir = "testdata"
tmp_dir = "tmp" tmp_dir = "tmp"
[env]
file = ".env"
[build] [build]
args_bin = ["-log-level=DEBUG"] args_bin = ["-log-level=DEBUG"]
bin = "./tmp/main" bin = "./tmp/main"

@ -0,0 +1,4 @@
export PULLEY_DATABASE_URI=postgres://pulley:passwd@localhost:5434/pulley
export PULLEY_PORT=5002
export PULLEY_LOG_LEVEL="debug"

3
.gitignore vendored

@ -21,6 +21,7 @@
# Go workspace file # Go workspace file
go.work go.work
# local environment setup
tmp/ tmp/
.pg_data/ .pg_data/
.env

@ -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

@ -100,7 +100,7 @@ func (app *application) healthCheckHandler(w http.ResponseWriter, r *http.Reques
env := envelope{ env := envelope{
"status": "available", "status": "available",
"system_info": map[string]string{ "system_info": map[string]string{
"environment": app.config.env, "environment": app.config.Env,
"version": Version, "version": Version,
}, },
} }

@ -12,10 +12,11 @@ import (
"testing" "testing"
"git.runcible.io/learning/pulley/internal/assert" "git.runcible.io/learning/pulley/internal/assert"
"git.runcible.io/learning/pulley/internal/config"
) )
func newTestApplication() application { func newTestApplication() application {
cfg := config{env: "test"} cfg := config.ServiceConfig{Env: "test"}
return application{config: cfg, logger: slog.New(slog.NewTextHandler(io.Discard, nil))} return application{config: cfg, logger: slog.New(slog.NewTextHandler(io.Discard, nil))}
} }

@ -3,7 +3,8 @@ package main
import ( import (
"context" "context"
"errors" "errors"
"flag"
// "flag"
"fmt" "fmt"
"io" "io"
"log/slog" "log/slog"
@ -13,40 +14,73 @@ import (
"syscall" "syscall"
"time" "time"
"git.runcible.io/learning/pulley/internal/config"
"git.runcible.io/learning/pulley/internal/logging" "git.runcible.io/learning/pulley/internal/logging"
"github.com/jackc/pgx/v5/pgxpool"
) )
const Version = "1.0.0" const Version = "1.0.0"
type config struct { //type config struct {
port int // port int
env string // env string
logLevel string // logLevel string
} //}
type application struct { type application struct {
config config config config.ServiceConfig
logger *slog.Logger logger *slog.Logger
} }
func run(ctx context.Context, w io.Writer, args []string) error { 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") pool, err := pgxpool.New(appCtx, cfg.DatabaseUri)
flagSet.StringVar(&cfg.env, "env", "development", "Environment (development|staging|production)") if err != nil {
flagSet.StringVar(&cfg.logLevel, "log-level", "INFO", "Logging Level (INFO|DEBUG|WARN|ERROR)") 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 return err
} }
logger := logging.InitLogging(cfg.logLevel, w, true)
app := application{config: cfg, logger: logger} app := application{config: cfg, logger: logger}
srv := &http.Server{ 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), ErrorLog: slog.NewLogLogger(app.logger.Handler(), slog.LevelError),
IdleTimeout: time.Minute, IdleTimeout: time.Minute,
ReadTimeout: 5 * time.Second, ReadTimeout: 5 * time.Second,
@ -70,12 +104,17 @@ func run(ctx context.Context, w io.Writer, args []string) error {
}() }()
// Handle graceful shutdown // 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 // 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() defer shutdownRelease()
if err := srv.Shutdown(shutdownCtx); err != nil { if err := srv.Shutdown(shutdownCtx); err != nil {

@ -3,3 +3,14 @@ module git.runcible.io/learning/pulley
go 1.24.1 go 1.24.1
require github.com/julienschmidt/httprouter v1.3.0 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
)

@ -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 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 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=

@ -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
}
Loading…
Cancel
Save