package main import ( "context" "fmt" "log" "net/http" "os" "time" "cloud.google.com/go/profiler" "github.com/gorilla/mux" "github.com/pkg/errors" "github.com/sirupsen/logrus" "go.opencensus.io/exporter/stackdriver" "go.opencensus.io/plugin/ocgrpc" "go.opencensus.io/plugin/ochttp" "go.opencensus.io/plugin/ochttp/propagation/b3" "go.opencensus.io/trace" "google.golang.org/grpc" ) const ( port = "8080" defaultCurrency = "USD" cookieMaxAge = 60 * 60 * 48 cookiePrefix = "shop_" cookieSessionID = cookiePrefix + "session-id" cookieCurrency = cookiePrefix + "currency" ) var ( whitelistedCurrencies = map[string]bool{ "USD": true, "EUR": true, "CAD": true, "JPY": true, "GBP": true, "TRY": true} ) type ctxKeySessionID struct{} type frontendServer struct { productCatalogSvcAddr string productCatalogSvcConn *grpc.ClientConn currencySvcAddr string currencySvcConn *grpc.ClientConn cartSvcAddr string cartSvcConn *grpc.ClientConn recommendationSvcAddr string recommendationSvcConn *grpc.ClientConn checkoutSvcAddr string checkoutSvcConn *grpc.ClientConn shippingSvcAddr string shippingSvcConn *grpc.ClientConn } func main() { ctx := context.Background() log := logrus.New() log.Level = logrus.DebugLevel log.Formatter = &logrus.TextFormatter{} go initProfiling("frontend", "1.0.0") go initTracing(log) srvPort := port if os.Getenv("PORT") != "" { srvPort = os.Getenv("PORT") } addr := os.Getenv("LISTEN_ADDR") svc := new(frontendServer) mustMapEnv(&svc.productCatalogSvcAddr, "PRODUCT_CATALOG_SERVICE_ADDR") mustMapEnv(&svc.currencySvcAddr, "CURRENCY_SERVICE_ADDR") mustMapEnv(&svc.cartSvcAddr, "CART_SERVICE_ADDR") mustMapEnv(&svc.recommendationSvcAddr, "RECOMMENDATION_SERVICE_ADDR") mustMapEnv(&svc.checkoutSvcAddr, "CHECKOUT_SERVICE_ADDR") mustMapEnv(&svc.shippingSvcAddr, "SHIPPING_SERVICE_ADDR") mustConnGRPC(ctx, &svc.currencySvcConn, svc.currencySvcAddr) mustConnGRPC(ctx, &svc.productCatalogSvcConn, svc.productCatalogSvcAddr) mustConnGRPC(ctx, &svc.cartSvcConn, svc.cartSvcAddr) mustConnGRPC(ctx, &svc.recommendationSvcConn, svc.recommendationSvcAddr) mustConnGRPC(ctx, &svc.shippingSvcConn, svc.shippingSvcAddr) mustConnGRPC(ctx, &svc.checkoutSvcConn, svc.checkoutSvcAddr) r := mux.NewRouter() r.HandleFunc("/", svc.homeHandler).Methods(http.MethodGet, http.MethodHead) r.HandleFunc("/product/{id}", svc.productHandler).Methods(http.MethodGet, http.MethodHead) r.HandleFunc("/cart", svc.viewCartHandler).Methods(http.MethodGet, http.MethodHead) r.HandleFunc("/cart", svc.addToCartHandler).Methods(http.MethodPost) r.HandleFunc("/cart/empty", svc.emptyCartHandler).Methods(http.MethodPost) r.HandleFunc("/setCurrency", svc.setCurrencyHandler).Methods(http.MethodPost) r.HandleFunc("/logout", svc.logoutHandler).Methods(http.MethodGet) r.HandleFunc("/cart/checkout", svc.placeOrderHandler).Methods(http.MethodPost) r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("./static/")))) var handler http.Handler = r handler = &logHandler{log: log, next: handler} // add logging handler = ensureSessionID(handler) // add session ID handler = &ochttp.Handler{ // add opencensus instrumentation Handler: handler, Propagation: &b3.HTTPFormat{}} log.Infof("starting server on " + addr + ":" + srvPort) log.Fatal(http.ListenAndServe(addr+":"+srvPort, handler)) } func initTracing(log logrus.FieldLogger) { // TODO(ahmetb) this method is duplicated in other microservices using Go // since they are not sharing packages. for i := 1; i <= 3; i++ { log = log.WithField("retry", i) exporter, err := stackdriver.NewExporter(stackdriver.Options{}) if err != nil { log.Warnf("failed to initialize stackdriver exporter: %+v", err) } else { trace.RegisterExporter(exporter) trace.ApplyConfig(trace.Config{DefaultSampler: trace.AlwaysSample()}) log.Info("registered stackdriver tracing") return } d := time.Second * 20 * time.Duration(i) log.Debugf("sleeping %v to retry initializing stackdriver exporter", d) time.Sleep(d) } log.Warn("could not initialize stackdriver exporter after retrying, giving up") } func initProfiling(service, version string) { // TODO(ahmetb) this method is duplicated in other microservices using Go // since they are not sharing packages. for i := 1; i <= 3; i++ { if err := profiler.Start(profiler.Config{ Service: service, ServiceVersion: version, // ProjectID must be set if not running on GCP. // ProjectID: "my-project", }); err != nil { log.Printf("warn: failed to start profiler: %+v", err) } d := time.Second * 10 * time.Duration(i) log.Printf("sleeping %v to retry initializing stackdriver profiler", d) time.Sleep(d) } log.Printf("warning: could not initialize stackdriver profiler after retrying, giving up") } func mustMapEnv(target *string, envKey string) { v := os.Getenv(envKey) if v == "" { panic(fmt.Sprintf("environment variable %q not set", envKey)) } *target = v } func mustConnGRPC(ctx context.Context, conn **grpc.ClientConn, addr string) { var err error *conn, err = grpc.DialContext(ctx, addr, grpc.WithInsecure(), grpc.WithTimeout(time.Second*3), grpc.WithStatsHandler(&ocgrpc.ClientHandler{})) if err != nil { panic(errors.Wrapf(err, "grpc: failed to connect %s", addr)) } }