2019-05-16 10:39:25 -04:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"crypto/rsa"
|
|
|
|
"encoding/json"
|
|
|
|
"expvar"
|
|
|
|
"io/ioutil"
|
|
|
|
"log"
|
|
|
|
"net/http"
|
|
|
|
_ "net/http/pprof"
|
|
|
|
"os"
|
|
|
|
"os/signal"
|
|
|
|
"syscall"
|
|
|
|
"time"
|
|
|
|
|
2019-05-16 18:05:39 -04:00
|
|
|
"geeks-accelerator/oss/saas-starter-kit/example-project/cmd/web-api/handlers"
|
2019-05-16 10:39:25 -04:00
|
|
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
|
|
|
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/db"
|
|
|
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/flag"
|
|
|
|
itrace "geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/trace"
|
|
|
|
jwt "github.com/dgrijalva/jwt-go"
|
|
|
|
"github.com/kelseyhightower/envconfig"
|
|
|
|
"go.opencensus.io/trace"
|
|
|
|
)
|
|
|
|
|
|
|
|
/*
|
|
|
|
ZipKin: http://localhost:9411
|
|
|
|
AddLoad: hey -m GET -c 10 -n 10000 "http://localhost:3000/v1/users"
|
|
|
|
expvarmon -ports=":3001" -endpoint="/metrics" -vars="requests,goroutines,errors,mem:memstats.Alloc"
|
|
|
|
*/
|
|
|
|
|
|
|
|
/*
|
|
|
|
Need to figure out timeouts for http service.
|
|
|
|
You might want to reset your DB_HOST env var during test tear down.
|
|
|
|
Service should start even without a DB running yet.
|
|
|
|
symbols in profiles: https://github.com/golang/go/issues/23376 / https://github.com/google/pprof/pull/366
|
|
|
|
*/
|
|
|
|
|
|
|
|
// build is the git version of this program. It is set using build flags in the makefile.
|
|
|
|
var build = "develop"
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
|
|
|
|
// =========================================================================
|
|
|
|
// Logging
|
|
|
|
|
2019-05-16 18:05:39 -04:00
|
|
|
log := log.New(os.Stdout, "WEB_APP : ", log.LstdFlags|log.Lmicroseconds|log.Lshortfile)
|
2019-05-16 10:39:25 -04:00
|
|
|
|
|
|
|
// =========================================================================
|
|
|
|
// Configuration
|
|
|
|
|
|
|
|
var cfg struct {
|
|
|
|
Web struct {
|
|
|
|
APIHost string `default:"0.0.0.0:3000" envconfig:"API_HOST"`
|
|
|
|
DebugHost string `default:"0.0.0.0:4000" envconfig:"DEBUG_HOST"`
|
|
|
|
ReadTimeout time.Duration `default:"5s" envconfig:"READ_TIMEOUT"`
|
|
|
|
WriteTimeout time.Duration `default:"5s" envconfig:"WRITE_TIMEOUT"`
|
|
|
|
ShutdownTimeout time.Duration `default:"5s" envconfig:"SHUTDOWN_TIMEOUT"`
|
|
|
|
}
|
|
|
|
DB struct {
|
|
|
|
DialTimeout time.Duration `default:"5s" envconfig:"DIAL_TIMEOUT"`
|
|
|
|
Host string `default:"mongo:27017/gotraining" envconfig:"HOST"`
|
|
|
|
}
|
|
|
|
Trace struct {
|
|
|
|
Host string `default:"http://tracer:3002/v1/publish" envconfig:"HOST"`
|
|
|
|
BatchSize int `default:"1000" envconfig:"BATCH_SIZE"`
|
|
|
|
SendInterval time.Duration `default:"15s" envconfig:"SEND_INTERVAL"`
|
|
|
|
SendTimeout time.Duration `default:"500ms" envconfig:"SEND_TIMEOUT"`
|
|
|
|
}
|
|
|
|
Auth struct {
|
|
|
|
KeyID string `envconfig:"KEY_ID"`
|
|
|
|
PrivateKeyFile string `default:"/app/private.pem" envconfig:"PRIVATE_KEY_FILE"`
|
|
|
|
Algorithm string `default:"RS256" envconfig:"ALGORITHM"`
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-16 18:05:39 -04:00
|
|
|
if err := envconfig.Process("WEB_APP", &cfg); err != nil {
|
2019-05-16 10:39:25 -04:00
|
|
|
log.Fatalf("main : Parsing Config : %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := flag.Process(&cfg); err != nil {
|
|
|
|
if err != flag.ErrHelp {
|
|
|
|
log.Fatalf("main : Parsing Command Line : %v", err)
|
|
|
|
}
|
|
|
|
return // We displayed help.
|
|
|
|
}
|
|
|
|
|
|
|
|
// =========================================================================
|
|
|
|
// App Starting
|
|
|
|
|
|
|
|
// Print the build version for our logs. Also expose it under /debug/vars.
|
|
|
|
expvar.NewString("build").Set(build)
|
|
|
|
log.Printf("main : Started : Application Initializing version %q", build)
|
|
|
|
defer log.Println("main : Completed")
|
|
|
|
|
|
|
|
cfgJSON, err := json.MarshalIndent(cfg, "", " ")
|
|
|
|
if err != nil {
|
|
|
|
log.Fatalf("main : Marshalling Config to JSON : %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: Validate what is being written to the logs. We don't
|
|
|
|
// want to leak credentials or anything that can be a security risk.
|
|
|
|
log.Printf("main : Config : %v\n", string(cfgJSON))
|
|
|
|
|
|
|
|
// =========================================================================
|
|
|
|
// Find auth keys
|
|
|
|
|
|
|
|
keyContents, err := ioutil.ReadFile(cfg.Auth.PrivateKeyFile)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatalf("main : Reading auth private key : %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
key, err := jwt.ParseRSAPrivateKeyFromPEM(keyContents)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatalf("main : Parsing auth private key : %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
publicKeyLookup := auth.NewSingleKeyFunc(cfg.Auth.KeyID, key.Public().(*rsa.PublicKey))
|
|
|
|
|
|
|
|
authenticator, err := auth.NewAuthenticator(key, cfg.Auth.KeyID, cfg.Auth.Algorithm, publicKeyLookup)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatalf("main : Constructing authenticator : %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// =========================================================================
|
|
|
|
// Start Mongo
|
|
|
|
|
|
|
|
log.Println("main : Started : Initialize Mongo")
|
|
|
|
masterDB, err := db.New(cfg.DB.Host, cfg.DB.DialTimeout)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatalf("main : Register DB : %v", err)
|
|
|
|
}
|
|
|
|
defer masterDB.Close()
|
|
|
|
|
|
|
|
// =========================================================================
|
|
|
|
// Start Tracing Support
|
|
|
|
|
|
|
|
logger := func(format string, v ...interface{}) {
|
|
|
|
log.Printf(format, v...)
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Printf("main : Tracing Started : %s", cfg.Trace.Host)
|
|
|
|
exporter, err := itrace.NewExporter(logger, cfg.Trace.Host, cfg.Trace.BatchSize, cfg.Trace.SendInterval, cfg.Trace.SendTimeout)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatalf("main : RegiTracingster : ERROR : %v", err)
|
|
|
|
}
|
|
|
|
defer func() {
|
|
|
|
log.Printf("main : Tracing Stopping : %s", cfg.Trace.Host)
|
|
|
|
batch, err := exporter.Close()
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("main : Tracing Stopped : ERROR : Batch[%d] : %v", batch, err)
|
|
|
|
} else {
|
|
|
|
log.Printf("main : Tracing Stopped : Flushed Batch[%d]", batch)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
trace.RegisterExporter(exporter)
|
|
|
|
trace.ApplyConfig(trace.Config{DefaultSampler: trace.AlwaysSample()})
|
|
|
|
|
|
|
|
// =========================================================================
|
|
|
|
// Start Debug Service. Not concerned with shutting this down when the
|
|
|
|
// application is being shutdown.
|
|
|
|
//
|
|
|
|
// /debug/vars - Added to the default mux by the expvars package.
|
|
|
|
// /debug/pprof - Added to the default mux by the net/http/pprof package.
|
|
|
|
go func() {
|
|
|
|
log.Printf("main : Debug Listening %s", cfg.Web.DebugHost)
|
|
|
|
log.Printf("main : Debug Listener closed : %v", http.ListenAndServe(cfg.Web.DebugHost, http.DefaultServeMux))
|
|
|
|
}()
|
|
|
|
|
|
|
|
// =========================================================================
|
|
|
|
// Start API Service
|
|
|
|
|
|
|
|
// Make a channel to listen for an interrupt or terminate signal from the OS.
|
|
|
|
// Use a buffered channel because the signal package requires it.
|
|
|
|
shutdown := make(chan os.Signal, 1)
|
|
|
|
signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM)
|
|
|
|
|
|
|
|
api := http.Server{
|
|
|
|
Addr: cfg.Web.APIHost,
|
|
|
|
Handler: handlers.API(shutdown, log, masterDB, authenticator),
|
|
|
|
ReadTimeout: cfg.Web.ReadTimeout,
|
|
|
|
WriteTimeout: cfg.Web.WriteTimeout,
|
|
|
|
MaxHeaderBytes: 1 << 20,
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make a channel to listen for errors coming from the listener. Use a
|
|
|
|
// buffered channel so the goroutine can exit if we don't collect this error.
|
|
|
|
serverErrors := make(chan error, 1)
|
|
|
|
|
|
|
|
// Start the service listening for requests.
|
|
|
|
go func() {
|
|
|
|
log.Printf("main : API Listening %s", cfg.Web.APIHost)
|
|
|
|
serverErrors <- api.ListenAndServe()
|
|
|
|
}()
|
|
|
|
|
|
|
|
// =========================================================================
|
|
|
|
// Shutdown
|
|
|
|
|
|
|
|
// Blocking main and waiting for shutdown.
|
|
|
|
select {
|
|
|
|
case err := <-serverErrors:
|
|
|
|
log.Fatalf("main : Error starting server: %v", err)
|
|
|
|
|
|
|
|
case sig := <-shutdown:
|
|
|
|
log.Printf("main : %v : Start shutdown..", sig)
|
|
|
|
|
|
|
|
// Create context for Shutdown call.
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), cfg.Web.ShutdownTimeout)
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
// Asking listener to shutdown and load shed.
|
|
|
|
err := api.Shutdown(ctx)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("main : Graceful shutdown did not complete in %v : %v", cfg.Web.ShutdownTimeout, err)
|
|
|
|
err = api.Close()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Log the status of this shutdown.
|
|
|
|
switch {
|
|
|
|
case sig == syscall.SIGSTOP:
|
|
|
|
log.Fatal("main : Integrity issue caused shutdown")
|
|
|
|
case err != nil:
|
|
|
|
log.Fatalf("main : Could not stop server gracefully : %v", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|