From ab045ae6e7a418736b9771cef28ce065f971af4b Mon Sep 17 00:00:00 2001 From: Ahmet Alp Balkan Date: Fri, 29 Jun 2018 13:45:03 -0700 Subject: [PATCH 01/13] currencyservice: change port to 7000 Signed-off-by: Ahmet Alp Balkan --- kubernetes-manifests/cartservice.yaml | 8 ++++---- kubernetes-manifests/currencyservice.yaml | 8 ++++---- src/currencyservice/Dockerfile | 4 ++-- src/currencyservice/server.js | 2 +- src/frontend/Dockerfile | 3 ++- 5 files changed, 13 insertions(+), 12 deletions(-) diff --git a/kubernetes-manifests/cartservice.yaml b/kubernetes-manifests/cartservice.yaml index a0d24f6..4d40548 100644 --- a/kubernetes-manifests/cartservice.yaml +++ b/kubernetes-manifests/cartservice.yaml @@ -20,10 +20,10 @@ spec: value: "7070" - name: LISTEN_ADDR value: "0.0.0.0" - - name: GRPC_TRACE - value: "all" - - name: GRPC_VERBOSITY - value: "debug" + # - name: GRPC_TRACE + # value: "all" + # - name: GRPC_VERBOSITY + # value: "debug" resources: requests: cpu: 200m diff --git a/kubernetes-manifests/currencyservice.yaml b/kubernetes-manifests/currencyservice.yaml index 4a4dc64..e7de91c 100644 --- a/kubernetes-manifests/currencyservice.yaml +++ b/kubernetes-manifests/currencyservice.yaml @@ -13,15 +13,15 @@ spec: - name: server image: currencyservice ports: - - containerPort: 31337 + - containerPort: 7000 readinessProbe: periodSeconds: 5 tcpSocket: - port: 31337 + port: 7000 livenessProbe: periodSeconds: 5 tcpSocket: - port: 31337 + port: 7000 resources: requests: cpu: 100m @@ -40,4 +40,4 @@ spec: app: currencyservice ports: - port: 7000 - targetPort: 31337 + targetPort: 7000 diff --git a/src/currencyservice/Dockerfile b/src/currencyservice/Dockerfile index 7eb074b..a87d34f 100644 --- a/src/currencyservice/Dockerfile +++ b/src/currencyservice/Dockerfile @@ -3,5 +3,5 @@ WORKDIR /usr/src/app COPY package*.json ./ RUN npm install --only=production COPY . . -EXPOSE 31337 -CMD [ "node", "server.js" ] \ No newline at end of file +EXPOSE 7000 +CMD [ "node", "server.js" ] diff --git a/src/currencyservice/server.js b/src/currencyservice/server.js index bd8f7f0..3274788 100644 --- a/src/currencyservice/server.js +++ b/src/currencyservice/server.js @@ -20,7 +20,7 @@ const request = require('request'); const xml2js = require('xml2js'); const PROTO_PATH = path.join(__dirname, './proto/demo.proto'); -const PORT = 31337; +const PORT = 7000; const DATA_URL = 'http://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml'; const shopProto = grpc.load(PROTO_PATH).hipstershop; diff --git a/src/frontend/Dockerfile b/src/frontend/Dockerfile index d3271fe..40f36ac 100644 --- a/src/frontend/Dockerfile +++ b/src/frontend/Dockerfile @@ -22,7 +22,8 @@ RUN go install . # --- FROM alpine as release -RUN apk add --no-cache ca-certificates +RUN apk add --no-cache ca-certificates \ + busybox-extras net-tools bind-tools WORKDIR /frontend COPY --from=builder /go/bin/frontend /frontend/server COPY ./templates ./templates From 8c3d36d81eb59f6b835d824c87a70a016468b7fd Mon Sep 17 00:00:00 2001 From: Ahmet Alp Balkan Date: Fri, 29 Jun 2018 16:35:57 -0700 Subject: [PATCH 02/13] loadgenerator prototype Signed-off-by: Ahmet Alp Balkan --- kubernetes-manifests/loadgenerator.yaml | 27 ++++++++++ skaffold.yaml | 2 + src/loadgenerator/Dockerfile | 7 +++ src/loadgenerator/locustfile.py | 66 +++++++++++++++++++++++++ src/loadgenerator/requirements.txt | 2 + 5 files changed, 104 insertions(+) create mode 100644 kubernetes-manifests/loadgenerator.yaml create mode 100644 src/loadgenerator/Dockerfile create mode 100644 src/loadgenerator/locustfile.py create mode 100644 src/loadgenerator/requirements.txt diff --git a/kubernetes-manifests/loadgenerator.yaml b/kubernetes-manifests/loadgenerator.yaml new file mode 100644 index 0000000..7fa40a0 --- /dev/null +++ b/kubernetes-manifests/loadgenerator.yaml @@ -0,0 +1,27 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: loadgenerator +spec: + template: + metadata: + labels: + app: loadgenerator + spec: + initContainers: + - name: wait-frontend + image: alpine:3.6 + command: ['sh', '-c', 'until wget -qO- "http://${FRONTEND_ADDR}"; do echo "waiting for ${FRONTEND_ADDR}"; sleep 2; done;'] + containers: + - name: server + image: loadgenerator + env: + - name: FRONTEND_ADDR + value: "frontend:8080" + resources: + requests: + cpu: 100m + memory: 64Mi + limits: + cpu: 200m + memory: 128Mi diff --git a/skaffold.yaml b/skaffold.yaml index 45f2fa6..fb88de3 100644 --- a/skaffold.yaml +++ b/skaffold.yaml @@ -22,6 +22,8 @@ build: workspace: src/cartservice - imageName: frontend workspace: src/frontend + - imageName: loadgenerator + workspace: src/loadgenerator deploy: kubectl: manifests: diff --git a/src/loadgenerator/Dockerfile b/src/loadgenerator/Dockerfile new file mode 100644 index 0000000..69cf7b3 --- /dev/null +++ b/src/loadgenerator/Dockerfile @@ -0,0 +1,7 @@ +FROM python:3 + +COPY requirements.txt . +RUN pip install -r requirements.txt + +COPY . . +ENTRYPOINT locust --host="http://${FRONTEND_ADDR}" --no-web -c=10 diff --git a/src/loadgenerator/locustfile.py b/src/loadgenerator/locustfile.py new file mode 100644 index 0000000..f56beba --- /dev/null +++ b/src/loadgenerator/locustfile.py @@ -0,0 +1,66 @@ +import random +from locust import HttpLocust, TaskSet + +products = [ + '0PUK6V6EV0', + '1YMWWN1N4O', + '2ZYFJ3GM2N', + '66VCHSJNUP', + '6E92ZMYYFZ', + '9SIQT8TOJO', + 'L9ECAV7KIM', + 'LS4PSXUNUM', + 'OLJCESPC7Z'] + +def index(l): + l.client.get("/") + +def setCurrency(l): + currencies = ['EUR', 'USD', 'JPY', 'CAD'] + l.client.post("/setCurrency", + {'currency_code': random.choice(currencies)}) + +def browseProduct(l): + l.client.get("/product/" + random.choice(products)) + +def viewCart(l): + l.client.get("/cart") + +def addToCart(l): + product = random.choice(products) + l.client.get("/product/" + product) + l.client.post("/cart", { + 'product_id': product, + 'quantity': random.choice([1,2,3,4,5,10])}) + +def checkout(l): + addToCart(l) + l.client.post("/cart/checkout", { + 'email': 'someone@example.com', + 'street_address': '1600 Amphitheatre Parkway', + 'zip_code': '94043', + 'city': 'Mountain View', + 'state': 'CA', + 'country': 'United States', + 'credit_card_number': '4432-8015-6152-0454', + 'credit_card_expiration_month': '1', + 'credit_card_expiration_year': '2019', + 'credit_card_cvv': '672', + }) + +class UserBehavior(TaskSet): + + def on_start(self): + index(self) + + tasks = {index: 1, + setCurrency: 2, + browseProduct: 10, + addToCart: 2, + viewCart: 3, + checkout: 1} + +class WebsiteUser(HttpLocust): + task_set = UserBehavior + min_wait = 1000 + max_wait = 2000 diff --git a/src/loadgenerator/requirements.txt b/src/loadgenerator/requirements.txt new file mode 100644 index 0000000..fa77c1c --- /dev/null +++ b/src/loadgenerator/requirements.txt @@ -0,0 +1,2 @@ +locustio==0.8.1 +pyzmq==17.0.0 From 08aa1cce261e7d3ff874e296fc134228283679ec Mon Sep 17 00:00:00 2001 From: Ahmet Alp Balkan Date: Fri, 29 Jun 2018 16:36:12 -0700 Subject: [PATCH 03/13] shippingservice: add logs indicating rpc calls Signed-off-by: Ahmet Alp Balkan --- src/shippingservice/main.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/shippingservice/main.go b/src/shippingservice/main.go index d6abd92..a946334 100644 --- a/src/shippingservice/main.go +++ b/src/shippingservice/main.go @@ -22,6 +22,8 @@ type server struct{} // GetQuote produces a shipping quote (cost) in USD. func (s *server) GetQuote(ctx context.Context, in *pb.GetQuoteRequest) (*pb.GetQuoteResponse, error) { + log.Printf("[GetQuote] received request") + defer log.Printf("[GetQuote] completed request") // 1. Our quote system requires the total number of items to be shipped. count := 0 @@ -45,6 +47,8 @@ func (s *server) GetQuote(ctx context.Context, in *pb.GetQuoteRequest) (*pb.GetQ // ShipOrder mocks that the requested items will be shipped. // It supplies a tracking ID for notional lookup of shipment delivery status. func (s *server) ShipOrder(ctx context.Context, in *pb.ShipOrderRequest) (*pb.ShipOrderResponse, error) { + log.Printf("[ShipOrder] received request") + defer log.Printf("[ShipOrder] completed request") // 1. Create a Tracking ID baseAddress := fmt.Sprintf("%s, %s, %s", in.Address.StreetAddress, in.Address.City, in.Address.State) id := CreateTrackingId(baseAddress) From 257cbdf98be8156b8f95e4b24340e77d520f4933 Mon Sep 17 00:00:00 2001 From: Ahmet Alp Balkan Date: Fri, 29 Jun 2018 17:10:28 -0700 Subject: [PATCH 04/13] loadgenerator: complete implementation Signed-off-by: Ahmet Alp Balkan --- kubernetes-manifests/loadgenerator.yaml | 17 ++++++++++++++--- src/loadgenerator/Dockerfile | 2 +- src/loadgenerator/loadgen.sh | 10 ++++++++++ src/loadgenerator/locustfile.py | 2 +- 4 files changed, 26 insertions(+), 5 deletions(-) create mode 100755 src/loadgenerator/loadgen.sh diff --git a/kubernetes-manifests/loadgenerator.yaml b/kubernetes-manifests/loadgenerator.yaml index 7fa40a0..e81d4a6 100644 --- a/kubernetes-manifests/loadgenerator.yaml +++ b/kubernetes-manifests/loadgenerator.yaml @@ -3,21 +3,32 @@ kind: Deployment metadata: name: loadgenerator spec: + replicas: 1 template: metadata: labels: app: loadgenerator spec: + restartPolicy: Always initContainers: - name: wait-frontend image: alpine:3.6 - command: ['sh', '-c', 'until wget -qO- "http://${FRONTEND_ADDR}"; do echo "waiting for ${FRONTEND_ADDR}"; sleep 2; done;'] + command: ['sh', '-c', 'set -x; apk add --no-cache curl; + until curl -f "http://${FRONTEND_ADDR}"; do + echo "waiting for http://${FRONTEND_ADDR}"; + sleep 2; + done;'] + env: + - name: FRONTEND_ADDR + value: "frontend:80" containers: - - name: server + - name: main image: loadgenerator env: - name: FRONTEND_ADDR - value: "frontend:8080" + value: "frontend:80" + - name: USERS + value: "1" resources: requests: cpu: 100m diff --git a/src/loadgenerator/Dockerfile b/src/loadgenerator/Dockerfile index 69cf7b3..be8e00b 100644 --- a/src/loadgenerator/Dockerfile +++ b/src/loadgenerator/Dockerfile @@ -4,4 +4,4 @@ COPY requirements.txt . RUN pip install -r requirements.txt COPY . . -ENTRYPOINT locust --host="http://${FRONTEND_ADDR}" --no-web -c=10 +ENTRYPOINT ./loadgen.sh diff --git a/src/loadgenerator/loadgen.sh b/src/loadgenerator/loadgen.sh new file mode 100755 index 0000000..5021e29 --- /dev/null +++ b/src/loadgenerator/loadgen.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -e + +if [[ -z "${FRONTEND_ADDR}" ]]; then + echo >&2 "FRONTEND_ADDR not specified" + exit 1 +fi + +set -x +locust --host="http://${FRONTEND_ADDR}" --no-web -c "${USERS:-10}" diff --git a/src/loadgenerator/locustfile.py b/src/loadgenerator/locustfile.py index f56beba..012cd48 100644 --- a/src/loadgenerator/locustfile.py +++ b/src/loadgenerator/locustfile.py @@ -63,4 +63,4 @@ class UserBehavior(TaskSet): class WebsiteUser(HttpLocust): task_set = UserBehavior min_wait = 1000 - max_wait = 2000 + max_wait = 10000 From b57cbd2c2c0d696cac8223aa5f93d8713b5ca98b Mon Sep 17 00:00:00 2001 From: Ahmet Alp Balkan Date: Sat, 30 Jun 2018 13:14:20 -0700 Subject: [PATCH 05/13] k8s: add termination period to loadgen to some svc Signed-off-by: Ahmet Alp Balkan --- kubernetes-manifests/loadgenerator.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/kubernetes-manifests/loadgenerator.yaml b/kubernetes-manifests/loadgenerator.yaml index e81d4a6..701dbbe 100644 --- a/kubernetes-manifests/loadgenerator.yaml +++ b/kubernetes-manifests/loadgenerator.yaml @@ -9,6 +9,7 @@ spec: labels: app: loadgenerator spec: + terminationGracePeriodSeconds: 5 restartPolicy: Always initContainers: - name: wait-frontend From b7404e3500c15b8bf8997da77584a1cbf3b1cb13 Mon Sep 17 00:00:00 2001 From: Ahmet Alp Balkan Date: Sun, 1 Jul 2018 21:21:26 -0700 Subject: [PATCH 06/13] frontend: use logrus structured logger Signed-off-by: Ahmet Alp Balkan --- src/frontend/Dockerfile | 1 + src/frontend/handlers.go | 106 ++++++++++------------ src/frontend/main.go | 15 +-- src/frontend/middleware.go | 85 +++++++++++++++++ src/frontend/port-forward-dependencies.sh | 2 +- 5 files changed, 142 insertions(+), 67 deletions(-) create mode 100644 src/frontend/middleware.go diff --git a/src/frontend/Dockerfile b/src/frontend/Dockerfile index 40f36ac..7ef2fe5 100644 --- a/src/frontend/Dockerfile +++ b/src/frontend/Dockerfile @@ -6,6 +6,7 @@ WORKDIR /go/src/frontend RUN go get -d github.com/google/uuid \ github.com/gorilla/mux \ github.com/pkg/errors \ + github.com/sirupsen/logrus \ google.golang.org/grpc \ google.golang.org/grpc/codes \ google.golang.org/grpc/status diff --git a/src/frontend/handlers.go b/src/frontend/handlers.go index 6bf45b0..cf3307e 100644 --- a/src/frontend/handlers.go +++ b/src/frontend/handlers.go @@ -1,18 +1,16 @@ package main import ( - "context" "fmt" "frontend/money" "html/template" - "log" "net/http" "strconv" "time" - "github.com/google/uuid" "github.com/gorilla/mux" "github.com/pkg/errors" + "github.com/sirupsen/logrus" pb "frontend/genproto" ) @@ -24,45 +22,22 @@ var ( }).ParseGlob("templates/*.html")) ) -func ensureSessionID(next http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - var sessionID string - c, err := r.Cookie(cookieSessionID) - if err == http.ErrNoCookie { - u, _ := uuid.NewRandom() - sessionID = u.String() - http.SetCookie(w, &http.Cookie{ - Name: cookieSessionID, - Value: sessionID, - MaxAge: cookieMaxAge, - }) - } else if err != nil { - renderHTTPError(w, errors.Wrap(err, "unrecognized cookie error"), http.StatusInternalServerError) - return - } else { - sessionID = c.Value - } - ctx := context.WithValue(r.Context(), ctxKeySessionID{}, sessionID) - r = r.WithContext(ctx) - next(w, r) - } -} - func (fe *frontendServer) homeHandler(w http.ResponseWriter, r *http.Request) { - log.Printf("[home] session_id=%s currency=%s", sessionID(r), currentCurrency(r)) + log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger) + log.WithField("currency", currentCurrency(r)).Info("home") currencies, err := fe.getCurrencies(r.Context()) if err != nil { - renderHTTPError(w, errors.Wrap(err, "could not retrieve currencies"), http.StatusInternalServerError) + renderHTTPError(log, w, errors.Wrap(err, "could not retrieve currencies"), http.StatusInternalServerError) return } products, err := fe.getProducts(r.Context()) if err != nil { - renderHTTPError(w, errors.Wrap(err, "could not retrieve products"), http.StatusInternalServerError) + renderHTTPError(log, w, errors.Wrap(err, "could not retrieve products"), http.StatusInternalServerError) return } cart, err := fe.getCart(r.Context(), sessionID(r)) if err != nil { - renderHTTPError(w, errors.Wrap(err, "could not retrieve cart"), http.StatusInternalServerError) + renderHTTPError(log, w, errors.Wrap(err, "could not retrieve cart"), http.StatusInternalServerError) return } @@ -74,7 +49,7 @@ func (fe *frontendServer) homeHandler(w http.ResponseWriter, r *http.Request) { for i, p := range products { price, err := fe.convertCurrency(r.Context(), p.GetPriceUsd(), currentCurrency(r)) if err != nil { - renderHTTPError(w, errors.Wrapf(err, "failed to do currency conversion for product %s", p.GetId()), http.StatusInternalServerError) + renderHTTPError(log, w, errors.Wrapf(err, "failed to do currency conversion for product %s", p.GetId()), http.StatusInternalServerError) return } ps[i] = productView{p, price} @@ -87,43 +62,46 @@ func (fe *frontendServer) homeHandler(w http.ResponseWriter, r *http.Request) { "session_id": sessionID(r), "cart_size": len(cart), }); err != nil { - log.Println(err) + log.Error(err) } } func (fe *frontendServer) productHandler(w http.ResponseWriter, r *http.Request) { + log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger) id := mux.Vars(r)["id"] if id == "" { - renderHTTPError(w, errors.New("product id not specified"), http.StatusBadRequest) + renderHTTPError(log, w, errors.New("product id not specified"), http.StatusBadRequest) return } - log.Printf("[productHandler] id=%s currency=%s session=%s", id, currentCurrency(r), sessionID(r)) + log.WithField("id", id).WithField("currency", currentCurrency(r)). + Debug("serving product page") + p, err := fe.getProduct(r.Context(), id) if err != nil { - renderHTTPError(w, errors.Wrap(err, "could not retrieve product"), http.StatusInternalServerError) + renderHTTPError(log, w, errors.Wrap(err, "could not retrieve product"), http.StatusInternalServerError) return } currencies, err := fe.getCurrencies(r.Context()) if err != nil { - renderHTTPError(w, errors.Wrap(err, "could not retrieve currencies"), http.StatusInternalServerError) + renderHTTPError(log, w, errors.Wrap(err, "could not retrieve currencies"), http.StatusInternalServerError) return } cart, err := fe.getCart(r.Context(), sessionID(r)) if err != nil { - renderHTTPError(w, errors.Wrap(err, "could not retrieve cart"), http.StatusInternalServerError) + renderHTTPError(log, w, errors.Wrap(err, "could not retrieve cart"), http.StatusInternalServerError) return } price, err := fe.convertCurrency(r.Context(), p.GetPriceUsd(), currentCurrency(r)) if err != nil { - renderHTTPError(w, errors.Wrap(err, "failed to convert currency"), http.StatusInternalServerError) + renderHTTPError(log, w, errors.Wrap(err, "failed to convert currency"), http.StatusInternalServerError) return } recommendations, err := fe.getRecommendations(r.Context(), sessionID(r), []string{id}) if err != nil { - renderHTTPError(w, errors.Wrap(err, "failed to get product recommendations"), http.StatusInternalServerError) + renderHTTPError(log, w, errors.Wrap(err, "failed to get product recommendations"), http.StatusInternalServerError) return } @@ -145,22 +123,23 @@ func (fe *frontendServer) productHandler(w http.ResponseWriter, r *http.Request) } func (fe *frontendServer) addToCartHandler(w http.ResponseWriter, r *http.Request) { + log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger) quantity, _ := strconv.ParseUint(r.FormValue("quantity"), 10, 32) productID := r.FormValue("product_id") if productID == "" || quantity == 0 { - renderHTTPError(w, errors.New("invalid form input"), http.StatusBadRequest) + renderHTTPError(log, w, errors.New("invalid form input"), http.StatusBadRequest) return } - log.Printf("[addToCart] product_id=%s qty=%d session_id=%s", productID, quantity, sessionID(r)) + log.WithField("product", productID).WithField("quantity", quantity).Debug("adding to cart") p, err := fe.getProduct(r.Context(), productID) if err != nil { - renderHTTPError(w, errors.Wrap(err, "could not retrieve product"), http.StatusInternalServerError) + renderHTTPError(log, w, errors.Wrap(err, "could not retrieve product"), http.StatusInternalServerError) return } if err := fe.insertCart(r.Context(), sessionID(r), p.GetId(), int32(quantity)); err != nil { - renderHTTPError(w, errors.Wrap(err, "failed to add to cart"), http.StatusInternalServerError) + renderHTTPError(log, w, errors.Wrap(err, "failed to add to cart"), http.StatusInternalServerError) return } w.Header().Set("location", "/cart") @@ -168,10 +147,11 @@ func (fe *frontendServer) addToCartHandler(w http.ResponseWriter, r *http.Reques } func (fe *frontendServer) emptyCartHandler(w http.ResponseWriter, r *http.Request) { - log.Printf("[emptyCart] session_id=%s", sessionID(r)) + log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger) + log.Debug("emptying cart") if err := fe.emptyCart(r.Context(), sessionID(r)); err != nil { - renderHTTPError(w, errors.Wrap(err, "failed to empty cart"), http.StatusInternalServerError) + renderHTTPError(log, w, errors.Wrap(err, "failed to empty cart"), http.StatusInternalServerError) return } w.Header().Set("location", "/") @@ -179,27 +159,28 @@ func (fe *frontendServer) emptyCartHandler(w http.ResponseWriter, r *http.Reques } func (fe *frontendServer) viewCartHandler(w http.ResponseWriter, r *http.Request) { - log.Printf("[viewCart] session_id=%s", sessionID(r)) + log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger) + log.Debug("view user cart") currencies, err := fe.getCurrencies(r.Context()) if err != nil { - renderHTTPError(w, errors.Wrap(err, "could not retrieve currencies"), http.StatusInternalServerError) + renderHTTPError(log, w, errors.Wrap(err, "could not retrieve currencies"), http.StatusInternalServerError) return } cart, err := fe.getCart(r.Context(), sessionID(r)) if err != nil { - renderHTTPError(w, errors.Wrap(err, "could not retrieve cart"), http.StatusInternalServerError) + renderHTTPError(log, w, errors.Wrap(err, "could not retrieve cart"), http.StatusInternalServerError) return } recommendations, err := fe.getRecommendations(r.Context(), sessionID(r), cartIDs(cart)) if err != nil { - renderHTTPError(w, errors.Wrap(err, "failed to get product recommendations"), http.StatusInternalServerError) + renderHTTPError(log, w, errors.Wrap(err, "failed to get product recommendations"), http.StatusInternalServerError) return } shippingCost, err := fe.getShippingQuote(r.Context(), cart, currentCurrency(r)) if err != nil { - renderHTTPError(w, errors.Wrap(err, "failed to get shipping quote"), http.StatusInternalServerError) + renderHTTPError(log, w, errors.Wrap(err, "failed to get shipping quote"), http.StatusInternalServerError) return } @@ -213,12 +194,12 @@ func (fe *frontendServer) viewCartHandler(w http.ResponseWriter, r *http.Request for i, item := range cart { p, err := fe.getProduct(r.Context(), item.GetProductId()) if err != nil { - renderHTTPError(w, errors.Wrapf(err, "could not retrieve product #%s", item.GetProductId()), http.StatusInternalServerError) + renderHTTPError(log, w, errors.Wrapf(err, "could not retrieve product #%s", item.GetProductId()), http.StatusInternalServerError) return } price, err := fe.convertCurrency(r.Context(), p.GetPriceUsd(), currentCurrency(r)) if err != nil { - renderHTTPError(w, errors.Wrapf(err, "could not convert currency for product #%s", item.GetProductId()), http.StatusInternalServerError) + renderHTTPError(log, w, errors.Wrapf(err, "could not convert currency for product #%s", item.GetProductId()), http.StatusInternalServerError) return } @@ -248,7 +229,8 @@ func (fe *frontendServer) viewCartHandler(w http.ResponseWriter, r *http.Request } func (fe *frontendServer) placeOrderHandler(w http.ResponseWriter, r *http.Request) { - log.Printf("[checkout] session_id=%s", sessionID(r)) + log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger) + log.Debug("placing order") var ( email = r.FormValue("email") @@ -281,10 +263,10 @@ func (fe *frontendServer) placeOrderHandler(w http.ResponseWriter, r *http.Reque Country: country}, }) if err != nil { - renderHTTPError(w, errors.Wrap(err, "failed to complete the order"), http.StatusInternalServerError) + renderHTTPError(log, w, errors.Wrap(err, "failed to complete the order"), http.StatusInternalServerError) return } - log.Printf("order #%s completed", order.GetOrder().GetOrderId()) + log.WithField("order", order.GetOrder().GetOrderId()).Info("order placed") order.GetOrder().GetItems() recommendations, _ := fe.getRecommendations(r.Context(), sessionID(r), nil) @@ -306,7 +288,8 @@ func (fe *frontendServer) placeOrderHandler(w http.ResponseWriter, r *http.Reque } func (fe *frontendServer) logoutHandler(w http.ResponseWriter, r *http.Request) { - log.Printf("[logout] session_id=%s", sessionID(r)) + log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger) + log.Debug("logging out") for _, c := range r.Cookies() { c.Expires = time.Now().Add(-time.Hour * 24 * 365) c.MaxAge = -1 @@ -317,8 +300,11 @@ func (fe *frontendServer) logoutHandler(w http.ResponseWriter, r *http.Request) } func (fe *frontendServer) setCurrencyHandler(w http.ResponseWriter, r *http.Request) { + log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger) cur := r.FormValue("currency_code") - log.Printf("[setCurrency] session_id=%s code=%s", sessionID(r), cur) + log.WithField("curr.new", cur).WithField("curr.old", currentCurrency(r)). + Debug("setting currency") + if cur != "" { http.SetCookie(w, &http.Cookie{ Name: cookieCurrency, @@ -334,8 +320,8 @@ func (fe *frontendServer) setCurrencyHandler(w http.ResponseWriter, r *http.Requ w.WriteHeader(http.StatusFound) } -func renderHTTPError(w http.ResponseWriter, err error, code int) { - log.Printf("[error](code=%d) %+v ", code, err) +func renderHTTPError(log logrus.FieldLogger, w http.ResponseWriter, err error, code int) { + log.WithField("error", err).Error("request error") errMsg := fmt.Sprintf("%+v", err) w.WriteHeader(code) diff --git a/src/frontend/main.go b/src/frontend/main.go index dcbe399..dc24284 100644 --- a/src/frontend/main.go +++ b/src/frontend/main.go @@ -3,14 +3,13 @@ package main import ( "context" "fmt" - "log" "net/http" "os" "time" - "github.com/pkg/errors" - "github.com/gorilla/mux" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" "google.golang.org/grpc" ) @@ -53,7 +52,9 @@ type frontendServer struct { func main() { ctx := context.Background() - log.SetFlags(log.Lshortfile | log.Ltime) + log := logrus.New() + log.Level = logrus.DebugLevel + log.Formatter = &logrus.TextFormatter{} srvPort := port if os.Getenv("PORT") != "" { @@ -85,8 +86,10 @@ func main() { r.HandleFunc("/logout", svc.logoutHandler).Methods(http.MethodGet) r.HandleFunc("/cart/checkout", ensureSessionID(svc.placeOrderHandler)).Methods(http.MethodPost) r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("./static/")))) - log.Printf("starting server on " + addr + ":" + srvPort) - log.Fatal(http.ListenAndServe(addr+":"+srvPort, r)) + + log.Infof("starting server on " + addr + ":" + srvPort) + loggedHandler := &logHandler{log: log, next: r} + log.Fatal(http.ListenAndServe(addr+":"+srvPort, loggedHandler)) } func mustMapEnv(target *string, envKey string) { diff --git a/src/frontend/middleware.go b/src/frontend/middleware.go new file mode 100644 index 0000000..41d4f58 --- /dev/null +++ b/src/frontend/middleware.go @@ -0,0 +1,85 @@ +package main + +import ( + "context" + "net/http" + "time" + + "github.com/google/uuid" + "github.com/sirupsen/logrus" +) + +type ctxKeyLog struct{} + +type logHandler struct { + log *logrus.Logger + next http.Handler +} + +type responseRecorder struct { + b int + status int + w http.ResponseWriter +} + +func (r *responseRecorder) Header() http.Header { return r.w.Header() } + +func (r *responseRecorder) Write(p []byte) (int, error) { + if r.status == 0 { + r.status = http.StatusOK + } + n, err := r.w.Write(p) + r.b += n + return n, err +} + +func (r *responseRecorder) WriteHeader(statusCode int) { + r.status = statusCode + r.w.WriteHeader(statusCode) +} + +func (lh *logHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + start := time.Now() + rr := &responseRecorder{w: w} + log := lh.log.WithFields(logrus.Fields{ + "http.req.path": r.URL.Path, + "http.req.method": r.Method, + }) + if v, ok := r.Context().Value(ctxKeySessionID{}).(string); ok { + log = log.WithField("session_id", v) + } + log.Debug("request started") + defer func() { + log.WithFields(logrus.Fields{ + "http.resp.took_ms": int64(time.Since(start) / time.Millisecond), + "http.resp.status": rr.status, + "http.resp.bytes": rr.b}).Debugf("request complete") + }() + + ctx := context.WithValue(r.Context(), ctxKeyLog{}, log) + r = r.WithContext(ctx) + lh.next.ServeHTTP(rr, r) +} + +func ensureSessionID(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var sessionID string + c, err := r.Cookie(cookieSessionID) + if err == http.ErrNoCookie { + u, _ := uuid.NewRandom() + sessionID = u.String() + http.SetCookie(w, &http.Cookie{ + Name: cookieSessionID, + Value: sessionID, + MaxAge: cookieMaxAge, + }) + } else if err != nil { + return + } else { + sessionID = c.Value + } + ctx := context.WithValue(r.Context(), ctxKeySessionID{}, sessionID) + r = r.WithContext(ctx) + next(w, r) + } +} diff --git a/src/frontend/port-forward-dependencies.sh b/src/frontend/port-forward-dependencies.sh index 4d7793d..dff1301 100755 --- a/src/frontend/port-forward-dependencies.sh +++ b/src/frontend/port-forward-dependencies.sh @@ -1,7 +1,7 @@ #!/bin/bash set -ex -kubectl port-forward $(kubectl get pods -l app=currencyservice -o=name | head -n 1) 7000:31337 & +kubectl port-forward $(kubectl get pods -l app=currencyservice -o=name | head -n 1) 7000:7000 & kubectl port-forward $(kubectl get pods -l app=recommendationservice -o=name | head -n 1) 8081:8080 & kubectl port-forward $(kubectl get pods -l app=cartservice -o=name | head -n 1) 7070:7070 & kubectl port-forward $(kubectl get pods -l app=productcatalogservice -o=name | head -n 1) 3550:3550 & From 2dcd7b6221cbac1c23550f71bb04324932771c42 Mon Sep 17 00:00:00 2001 From: Ahmet Alp Balkan Date: Mon, 2 Jul 2018 10:00:10 -0700 Subject: [PATCH 07/13] frontend: remove sessionid from log Signed-off-by: Ahmet Alp Balkan --- src/frontend/middleware.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/frontend/middleware.go b/src/frontend/middleware.go index 41d4f58..b8957e2 100644 --- a/src/frontend/middleware.go +++ b/src/frontend/middleware.go @@ -45,9 +45,6 @@ func (lh *logHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { "http.req.path": r.URL.Path, "http.req.method": r.Method, }) - if v, ok := r.Context().Value(ctxKeySessionID{}).(string); ok { - log = log.WithField("session_id", v) - } log.Debug("request started") defer func() { log.WithFields(logrus.Fields{ From 85d04fc0b50a9ee15d52a018437000b2fae677da Mon Sep 17 00:00:00 2001 From: Ahmet Alp Balkan Date: Mon, 2 Jul 2018 10:13:02 -0700 Subject: [PATCH 08/13] frontend: proper ensureSess middleware, add req id Signed-off-by: Ahmet Alp Balkan --- src/frontend/main.go | 17 +++++++++-------- src/frontend/middleware.go | 15 ++++++++++++--- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/frontend/main.go b/src/frontend/main.go index dc24284..f2b6cab 100644 --- a/src/frontend/main.go +++ b/src/frontend/main.go @@ -77,19 +77,20 @@ func main() { mustConnGRPC(ctx, &svc.checkoutSvcConn, svc.checkoutSvcAddr) r := mux.NewRouter() - r.HandleFunc("/", ensureSessionID(svc.homeHandler)).Methods(http.MethodGet, http.MethodHead) - r.HandleFunc("/product/{id}", ensureSessionID(svc.productHandler)).Methods(http.MethodGet, http.MethodHead) - r.HandleFunc("/cart", ensureSessionID(svc.viewCartHandler)).Methods(http.MethodGet, http.MethodHead) - r.HandleFunc("/cart", ensureSessionID(svc.addToCartHandler)).Methods(http.MethodPost) - r.HandleFunc("/cart/empty", ensureSessionID(svc.emptyCartHandler)).Methods(http.MethodPost) - r.HandleFunc("/setCurrency", ensureSessionID(svc.setCurrencyHandler)).Methods(http.MethodPost) + 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", ensureSessionID(svc.placeOrderHandler)).Methods(http.MethodPost) + r.HandleFunc("/cart/checkout", svc.placeOrderHandler).Methods(http.MethodPost) r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("./static/")))) log.Infof("starting server on " + addr + ":" + srvPort) loggedHandler := &logHandler{log: log, next: r} - log.Fatal(http.ListenAndServe(addr+":"+srvPort, loggedHandler)) + log.Fatal(http.ListenAndServe(addr+":"+srvPort, + http.HandlerFunc(ensureSessionID(loggedHandler)))) } func mustMapEnv(target *string, envKey string) { diff --git a/src/frontend/middleware.go b/src/frontend/middleware.go index b8957e2..a736dbe 100644 --- a/src/frontend/middleware.go +++ b/src/frontend/middleware.go @@ -10,6 +10,7 @@ import ( ) type ctxKeyLog struct{} +type ctxKeyRequestID struct{} type logHandler struct { log *logrus.Logger @@ -39,12 +40,20 @@ func (r *responseRecorder) WriteHeader(statusCode int) { } func (lh *logHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + requestID, _ := uuid.NewRandom() + ctx = context.WithValue(ctx, ctxKeyRequestID{}, requestID.String()) + start := time.Now() rr := &responseRecorder{w: w} log := lh.log.WithFields(logrus.Fields{ "http.req.path": r.URL.Path, "http.req.method": r.Method, + "http.req.id": requestID.String(), }) + if v, ok := r.Context().Value(ctxKeySessionID{}).(string); ok { + log = log.WithField("session", v) + } log.Debug("request started") defer func() { log.WithFields(logrus.Fields{ @@ -53,12 +62,12 @@ func (lh *logHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { "http.resp.bytes": rr.b}).Debugf("request complete") }() - ctx := context.WithValue(r.Context(), ctxKeyLog{}, log) + ctx = context.WithValue(ctx, ctxKeyLog{}, log) r = r.WithContext(ctx) lh.next.ServeHTTP(rr, r) } -func ensureSessionID(next http.HandlerFunc) http.HandlerFunc { +func ensureSessionID(next http.Handler) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var sessionID string c, err := r.Cookie(cookieSessionID) @@ -77,6 +86,6 @@ func ensureSessionID(next http.HandlerFunc) http.HandlerFunc { } ctx := context.WithValue(r.Context(), ctxKeySessionID{}, sessionID) r = r.WithContext(ctx) - next(w, r) + next.ServeHTTP(w, r) } } From e27dcc3883dcc2bc01bb5ac1936970774ab1e52a Mon Sep 17 00:00:00 2001 From: Ahmet Alp Balkan Date: Mon, 2 Jul 2018 10:24:11 -0700 Subject: [PATCH 09/13] Add README.md with installation instructions Signed-off-by: Ahmet Alp Balkan --- README.md | 29 +++++++++++++++++++++++++++++ kubernetes-manifests/frontend.yaml | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e460f59..454bd84 100644 --- a/README.md +++ b/README.md @@ -1 +1,30 @@ # Microservices demo + +This project contains a 10-tier microservices application. The application is a +web-based e-commerce app called “Hipster Shop” where users can browse items, +add them to the cart, and purchase them. + +### Setup on GKE + +0. Make sure you have a Google Kubernetes Engine cluster and enabled Google + Container Registry (GCR) on your GCP project: + + gcloud services enable containerregistry.googleapis.com + +1. Edit `skaffold.yaml`, prepend your GCR registry host (`gcr.io/YOUR_PROJECT/`) + to all `imageName:` fields. + +2. Edit the Deployment manifests at `kubernetes-manifests` directory and update + the image names to match the changes you made in the previous step. + +3. Install [Skaffold] and `skaffold run`. This builds the container + images, pushes them to GFR, and deploys the application to Kubernetes. + +4. Find the IP address of your application: + + kubectl get service frontend-external + + then visit the application on your browser to confirm + installation. + +[Skaffold]: https://github.com/GoogleContainerTools/skaffold/#installation diff --git a/kubernetes-manifests/frontend.yaml b/kubernetes-manifests/frontend.yaml index a09742e..ffd0392 100644 --- a/kubernetes-manifests/frontend.yaml +++ b/kubernetes-manifests/frontend.yaml @@ -71,5 +71,5 @@ spec: selector: app: frontend ports: - - port: 8081 + - port: 80 targetPort: 8080 From 78e745507a43c9b664ee234660ca3e79d23ac0cf Mon Sep 17 00:00:00 2001 From: Ahmet Alp Balkan Date: Mon, 2 Jul 2018 10:30:54 -0700 Subject: [PATCH 10/13] README fix Signed-off-by: Ahmet Alp Balkan --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 454bd84..886267e 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ add them to the cart, and purchase them. to all `imageName:` fields. 2. Edit the Deployment manifests at `kubernetes-manifests` directory and update - the image names to match the changes you made in the previous step. + the `image` fields to match the changes you made in the previous step. 3. Install [Skaffold] and `skaffold run`. This builds the container images, pushes them to GFR, and deploys the application to Kubernetes. From 2a251779e6b5e73f5f00292ca438ff42ce288463 Mon Sep 17 00:00:00 2001 From: Ahmet Alp Balkan Date: Mon, 2 Jul 2018 11:03:55 -0700 Subject: [PATCH 11/13] improve README.md Signed-off-by: Ahmet Alp Balkan --- README.md | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 886267e..1cebee1 100644 --- a/README.md +++ b/README.md @@ -6,25 +6,36 @@ add them to the cart, and purchase them. ### Setup on GKE -0. Make sure you have a Google Kubernetes Engine cluster and enabled Google - Container Registry (GCR) on your GCP project: +1. Install: + + - [gcloud](https://cloud.google.com/sdk/) + sign in to your account/project. + - kubectl (can be installed via `gcloud components install kubectl`) + - Docker (on Mac/Windows, install Docker for Desktop CE) + - [Skaffold](https://github.com/GoogleContainerTools/skaffold/#installation) + +1. Create a Google Kubernetes Engine cluster and make sure `kubectl` is pointing + to the cluster. + +1. Enable Google Container Registry (GCR) on your GCP project: gcloud services enable containerregistry.googleapis.com + +1. Configure docker to authenticate to GCR: + + gcloud auth configure-docker -q 1. Edit `skaffold.yaml`, prepend your GCR registry host (`gcr.io/YOUR_PROJECT/`) to all `imageName:` fields. -2. Edit the Deployment manifests at `kubernetes-manifests` directory and update +1. Edit the Deployment manifests at `kubernetes-manifests` directory and update the `image` fields to match the changes you made in the previous step. -3. Install [Skaffold] and `skaffold run`. This builds the container +1. Run `skaffold run`. This builds the container images, pushes them to GFR, and deploys the application to Kubernetes. -4. Find the IP address of your application: +1. Find the IP address of your application: kubectl get service frontend-external then visit the application on your browser to confirm installation. - -[Skaffold]: https://github.com/GoogleContainerTools/skaffold/#installation From 3416aee7d52557ff7f54c02b2565fdf266fb159b Mon Sep 17 00:00:00 2001 From: Ahmet Alp Balkan Date: Mon, 2 Jul 2018 12:41:33 -0700 Subject: [PATCH 12/13] k8s/cartservice: add init container to wait redis otherwise nullpointerexception persists Signed-off-by: Ahmet Alp Balkan --- kubernetes-manifests/cartservice.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/kubernetes-manifests/cartservice.yaml b/kubernetes-manifests/cartservice.yaml index 4d40548..ca2f649 100644 --- a/kubernetes-manifests/cartservice.yaml +++ b/kubernetes-manifests/cartservice.yaml @@ -8,6 +8,20 @@ spec: labels: app: cartservice spec: + terminationGracePeriodSeconds: 5 + initContainers: + - name: wait-redis + image: redis:alpine + command: ['sh', '-c', 'set -x; + until timeout -t 5 redis-cli -h "${REDIS_HOST}" -p "${REDIS_PORT}" ping; do + echo "waiting for redis at ${REDIS_HOST}:${REDIS_PORT}..."; + sleep 2; + done;'] + env: + - name: REDIS_HOST + value: "redis-cart" + - name: REDIS_PORT + value: "6379" containers: - name: server image: cartservice From 5cc013952e64bcc20481dd0528b82b74250ed6ab Mon Sep 17 00:00:00 2001 From: Ahmet Alp Balkan Date: Mon, 2 Jul 2018 13:03:37 -0700 Subject: [PATCH 13/13] remove trailing lines Signed-off-by: Ahmet Alp Balkan --- kubernetes-manifests/cartservice.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kubernetes-manifests/cartservice.yaml b/kubernetes-manifests/cartservice.yaml index ca2f649..82553cf 100644 --- a/kubernetes-manifests/cartservice.yaml +++ b/kubernetes-manifests/cartservice.yaml @@ -13,8 +13,8 @@ spec: - name: wait-redis image: redis:alpine command: ['sh', '-c', 'set -x; - until timeout -t 5 redis-cli -h "${REDIS_HOST}" -p "${REDIS_PORT}" ping; do - echo "waiting for redis at ${REDIS_HOST}:${REDIS_PORT}..."; + until timeout -t 5 redis-cli -h "${REDIS_HOST}" -p "${REDIS_PORT}" ping; do + echo "waiting for redis at ${REDIS_HOST}:${REDIS_PORT}..."; sleep 2; done;'] env: