package main import ( "context" "database/sql" "expvar" "flag" "os" "runtime" "strings" "sync" "time" "github.com/joho/godotenv" _ "github.com/lib/pq" "greenlight.debuggingjon.dev/internal/data" "greenlight.debuggingjon.dev/internal/jsonlog" "greenlight.debuggingjon.dev/internal/mailer" ) // Declare a string containing the application version number. Later in the book we'll // generate this automatically at build time, but for now we'll just store the version // number as a hard-coded global constant. const version = "1.0.0" // Define a config struct to hold all the configuration settings for our application. // For now, the only configuration settings will be the network port that we want the // server to listen on, and the name of the current operating environment for the // application (development, staging, production, etc.). We will read in these // configuration settings from command-line flags when the application starts. type config struct { port int env string db struct { dsn string maxOpenConns int maxIdleConns int maxIdleTime string } // Add a new limiter struct containing fields for the requests-per-second and burst // values, and a boolean field which we can use to enable/disable rate limiting // altogether. limiter struct { rps float64 burst int enabled bool } smtp struct { host string port int username string password string sender string } // Add a cors struct and trustedOrigins field with the type []string. cors struct { trustedOrigins []string } } // Define an application struct to hold the dependencies for our HTTP handlers, helpers, // and middleware. At the moment this only contains a copy of the config struct and a // logger, but it will grow to include a lot more as our build progresses. type application struct { config config logger *jsonlog.Logger models data.Models mailer mailer.Mailer wg sync.WaitGroup } func main() { var cfg config // Initialize a new jsonlog.Logger which writes any messages *at or above* the INFO // severity level to the standard out stream. logger := jsonlog.New(os.Stdout, jsonlog.LevelInfo) err := godotenv.Load() if err != nil { logger.PrintFatal(err, nil) } dsn := os.Getenv("DATABASE_DSN") flag.IntVar(&cfg.port, "port", 4000, "API server port") flag.StringVar(&cfg.env, "env", "development", "Environment (development|staging|production)") // Read the DSN value from the db-dsn command-line flag into the config struct. We // default to using our development DSN if no flag is provided. flag.StringVar(&cfg.db.dsn, "db-dsn", dsn, "PostgreSQL DSN (Default from DATABASE_DSN in .env)") // Read the connection pool settings from command-line flags into the config struct. // Notice the default values that we're using? flag.IntVar(&cfg.db.maxOpenConns, "db-max-open-conns", 25, "PostgreSQL max open connections") flag.IntVar(&cfg.db.maxIdleConns, "db-max-idle-conns", 25, "PostgreSQL max idle connections") flag.StringVar(&cfg.db.maxIdleTime, "db-max-idle-time", "15m", "PostgreSQL max connection idle time") // Create command line flags to read the setting values into the config struct. // Notice that we use true as the default for the 'enabled' setting? flag.Float64Var(&cfg.limiter.rps, "limiter-rps", 2, "Rate limiter maximum requests per second") flag.IntVar(&cfg.limiter.burst, "limiter-burst", 4, "Rate limiter maximum burst") flag.BoolVar(&cfg.limiter.enabled, "limiter-enabled", true, "Enable rate limiter") // Read the SMTP server configuration settings into the config struct, using the // Mailtrap settings as the default values. IMPORTANT: If you're following along, // make sure to replace the default values for smtp-username and smtp-password // with your own Mailtrap credentials. flag.StringVar(&cfg.smtp.host, "smtp-host", "sandbox.smtp.mailtrap.io", "SMTP host") flag.IntVar(&cfg.smtp.port, "smtp-port", 2525, "SMTP port") flag.StringVar(&cfg.smtp.username, "smtp-username", "3d946287da35ea", "SMTP username") flag.StringVar(&cfg.smtp.password, "smtp-password", "d06a774b484ca3", "SMTP password") flag.StringVar(&cfg.smtp.sender, "smtp-sender", "Greenlight ", "SMTP sender") // Use the flag.Func() function to process the -cors-trusted-origins command line // flag. In this we use the strings.Fields() function to split the flag value into a // slice based on whitespace characters and assign it to our config struct. // Importantly, if the -cors-trusted-origins flag is not present, contains the empty // string, or contains only whitespace, then strings.Fields() will return an empty // []string slice. flag.Func("cors-trusted-origins", "Trusted CORS origins (space separated)", func(val string) error { cfg.cors.trustedOrigins = strings.Fields(val) return nil }) flag.Parse() // Call the openDB() helper function (see below) to create the connection pool, // passing in the config struct. If this returns an error, we log it and exit the // application immediately. db, err := openDB(cfg) if err != nil { logger.PrintFatal(err, nil) } // Defer a call to db.Close() so that the connection pool is closed before the // main() function exits. defer func() { if closeErr := db.Close(); closeErr != nil && err == nil { err = closeErr } }() // Also log a message to say that the connection pool has been successfully // established. logger.PrintInfo("database connection pool established", nil) // Publish a new "version" variable in the expvar handler containing our application // version number (currently the constant "1.0.0"). expvar.NewString("version").Set(version) expvar.Publish("goroutines", expvar.Func(func() any { return runtime.NumGoroutine() })) // Publish the database connection pool statistics. expvar.Publish("database", expvar.Func(func() any { return db.Stats() })) // Publish the current Unix timestamp. expvar.Publish("timestamp", expvar.Func(func() any { return time.Now().Unix() })) app := &application{ config: cfg, logger: logger, models: data.NewModels(db), mailer: mailer.New(cfg.smtp.host, cfg.smtp.port, cfg.smtp.username, cfg.smtp.password, cfg.smtp.sender), } err = app.serve() if err != nil { logger.PrintFatal(err, nil) } } func openDB(cfg config) (*sql.DB, error) { db, err := sql.Open("postgres", cfg.db.dsn) if err != nil { return nil, err } // Set the maximum number of open (in-use + idle) connections in the pool. Note that // passing a value less than or equal to 0 will mean there is no limit. db.SetMaxOpenConns(cfg.db.maxOpenConns) // Set the maximum number of idle connections in the pool. Again, passing a value // less than or equal to 0 will mean there is no limit. db.SetMaxIdleConns(cfg.db.maxIdleConns) // Use the time.ParseDuration() function to convert the idle timeout duration string // to a time.Duration type. duration, err := time.ParseDuration(cfg.db.maxIdleTime) if err != nil { return nil, err } // Set the maximum idle timeout. db.SetConnMaxIdleTime(duration) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() err = db.PingContext(ctx) if err != nil { return nil, err } return db, nil }