env
env is for environment related items
Code Walkthrough
The env module is for returning an environment struct (Env) as well as setup/read functions and methods for all the components held within.
The Env struct:
// Env struct stores common environment related items
type Env struct {
// Environment Name (e.g. Production, QA, etc.)
Name Name
// multiplex router
Router *mux.Router
// Datastore struct containing AppDB (PostgreSQL),
// LogDb (PostgreSQL) and CacheDB (Redis)
DS *datastore.Datastore
// Logger
Logger zerolog.Logger
}
The env.Name type is defined as uint8
// Name is the environment Name int representation
// Using iota, 1 (Production) is the lowest,
// 2 (Staging) is 2nd lowest, and so on...
type Name uint8
Constants using iota are used to set the value of each environment.
// Name of environment.
const (
Production Name = iota + 1 // Production (1)
Staging // Staging (2)
QA // QA (3)
Dev // Dev (4)
)
The String method of env.Name is used to get the string value of the environment.
func (n Name) String() string {
switch n {
case Production:
return "Production"
case Staging:
return "Staging"
case QA:
return "QA"
case Dev:
return "Dev"
}
return "unknown_name"
}
The constructor function for Env is NewEnv in the env package. NewEnv first validates the inputs to ensure you're not passing a bogus log level or environment.
// NewEnv initializes the Env struct
func NewEnv(nme Name, lvl zerolog.Level) (*Env, error) {
const op errors.Op = "env/NewEnv"
if nme.String() == "unknown_name" {
return nil, errors.E(op, "Unknown env.Name input")
}
if lvl.String() == "" {
return nil, errors.E(op, "Unknown logger level input")
}
Then sets up the logger
// setup logger
log := newLogger(lvl)
using the newLogger function which sets the logging time format to use Unix time and send output to Stdout.
// newLogger sets up the zerolog.Logger
func newLogger(lvl zerolog.Level) zerolog.Logger {
// empty string for TimeFieldFormat will write logs with UNIX time
zerolog.TimeFieldFormat = ""
// set logging level based on input
zerolog.SetGlobalLevel(lvl)
// start a new logger with Stdout as the target
lgr := zerolog.New(os.Stdout).With().Timestamp().Logger()
return lgr
}
Next, a new Datastore is initialized using the NewDatastore constructor in the env/datastore package.
// open db connection pools
dstore, err := datastore.NewDatastore()
if err != nil {
return nil, errors.E(op, err)
}
Datastore is a struct for holding database related components.
// Datastore struct stores common environment related items
type Datastore struct {
appDB *sql.DB
logDB *sql.DB
cacheDB *redis.Pool
}
The NewDatastore constructor gets an open database handle only for the AppDB and sets it into the Datastore struct. The logDB and cachedB are deliberately left nil as they are only set through the Option method of Datastore. If you don't want to use a logger or redis database, you would not bother to set logDB and cachedB.
func NewDatastore() (*Datastore, error) {
const op errors.Op = "env/datastore/NewDatastore"
// Get an AppDB (PostgreSQL)
adb, err := newDB(AppDB)
if err != nil {
return nil, errors.E(op, err)
}
return &Datastore{appDB: adb, logDB: nil, cacheDB: nil}, nil
}
The newDB function called above does the work of opening the database handle to establish connection. None of the database credentials are in the code, they are all taken from environment variables on the operating system.
func newDB(n DBName) (*sql.DB, error) {
const op errors.Op = "env/datastore/newDB"
// Get Database connection credentials from environment variables
dbNme := os.Getenv(dbEnvName(n))
dbUser := os.Getenv(dbEnvUser(n))
dbPassword := os.Getenv(dbEnvPassword(n))
dbHost := os.Getenv(dbEnvHost(n))
dbPort, err := strconv.Atoi(os.Getenv(dbEnvPort(n)))
if err != nil {
return nil, errors.E(op, err)
}
// Craft string for database connection
dbinfo := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", dbHost, dbPort, dbUser, dbPassword, dbNme)
// Open the postgres database using the postgres driver (pq)
// func Open(driverName, dataSourceName string) (*DB, error)
db, err := sql.Open("postgres", dbinfo)
if err != nil {
return nil, errors.E(op, err)
}
// Call Ping to validate the newly opened database is actually alive
if err = db.Ping(); err != nil {
return nil, errors.E(op, err)
}
return db, nil
}
The environment variable names for the PostgreSQL (PG) database connections are below.
// Environment variables for the App DB PostgreSQL Database
const (
envAppDBName = "PG_APP_DBNAME"
envAppDBUser = "PG_APP_USERNAME"
envAppDBPassword = "PG_APP_PASSWORD"
envAppDBHost = "PG_APP_HOST"
envAppDBPort = "PG_APP_PORT"
)
// Environment variables for the Log DB PostgreSQL Database
const (
envLogDBName = "PG_LOG_DBNAME"
envLogDBUser = "PG_LOG_USERNAME"
envLogDBPassword = "PG_LOG_PASSWORD"
envLogDBHost = "PG_LOG_HOST"
envLogDBPort = "PG_LOG_PORT"
)
After the Datastore is initialized, a new HTTP request multiplexer router is initialized using mux.NewRouter from the Gorilla mux package.
// create a new mux (multiplex) router
rtr := mux.NewRouter()
Sub routes are added to the router
// send Router through subRouter function to add any standard
// Subroutes you may want for your APIs
rtr = newSubrouter(rtr)
using the newSubrouter function
// newSubrouter adds any subRouters that you'd like to have as part of
// every request, i.e. I always want to be sure that every request has
// "/api" as part of it's path prefix without having to put it into
// every handle path in my various routing functions
func newSubrouter(rtr *mux.Router) *mux.Router {
sRtr := rtr.PathPrefix("/api").Subrouter()
return sRtr
}
Finally, the Env struct is initialized and returned to the caller
env := &Env{Name: nme, Router: rtr, DS: dstore, Logger: log}
return env, nil
}