diff --git a/CHANGELOG.md b/CHANGELOG.md index 0800ae5810..dbfd016e99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,3 +15,5 @@ release. future significant modifications will be credited to OpenTelemetry Authors. * Added feature flag service protos ([#26](https://github.com/open-telemetry/opentelemetry-demo-webstore/pull/26)) +* Added span attributes to frontend service + ([#82](https://github.com/open-telemetry/opentelemetry-demo-webstore/pull/82)) diff --git a/src/frontend/handlers.go b/src/frontend/handlers.go index bea872679c..6b36b27d48 100644 --- a/src/frontend/handlers.go +++ b/src/frontend/handlers.go @@ -17,6 +17,10 @@ package main import ( "context" "fmt" + "github.com/GoogleCloudPlatform/microservices-demo/src/frontend/instr" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" "html/template" "math/rand" "net" @@ -102,6 +106,12 @@ func (fe *frontendServer) homeHandler(w http.ResponseWriter, r *http.Request) { plat = platformDetails{} plat.setPlatformDetails(strings.ToLower(env)) + span := trace.SpanFromContext(r.Context()) + span.SetAttributes( + attribute.Int(instr.AppPrefix+"products.count", len(products)), + attribute.Int(instr.AppPrefix+"cart.size", cartSize(cart)), + ) + if err := templates.ExecuteTemplate(w, "home", map[string]interface{}{ "session_id": sessionID(r), "request_id": r.Context().Value(ctxKeyRequestID{}), @@ -117,6 +127,7 @@ func (fe *frontendServer) homeHandler(w http.ResponseWriter, r *http.Request) { "is_cymbal_brand": isCymbalBrand, "deploymentDetails": deploymentDetailsMap, }); err != nil { + span.SetStatus(codes.Error, err.Error()) log.Error(err) } } @@ -187,6 +198,12 @@ func (fe *frontendServer) productHandler(w http.ResponseWriter, r *http.Request) Price *pb.Money }{p, price} + span := trace.SpanFromContext(r.Context()) + span.SetAttributes( + attribute.String(instr.AppPrefix+"product.id", id), + attribute.Int(instr.AppPrefix+"cart.size", cartSize(cart)), + ) + if err := templates.ExecuteTemplate(w, "product", map[string]interface{}{ "session_id": sessionID(r), "request_id": r.Context().Value(ctxKeyRequestID{}), @@ -202,6 +219,7 @@ func (fe *frontendServer) productHandler(w http.ResponseWriter, r *http.Request) "is_cymbal_brand": isCymbalBrand, "deploymentDetails": deploymentDetailsMap, }); err != nil { + span.SetStatus(codes.Error, err.Error()) log.Println(err) } } @@ -222,6 +240,12 @@ func (fe *frontendServer) addToCartHandler(w http.ResponseWriter, r *http.Reques return } + span := trace.SpanFromContext(r.Context()) + span.SetAttributes( + attribute.String(instr.AppPrefix+"product.id", productID), + attribute.Int(instr.AppPrefix+"product.quantity", int(quantity)), + ) + if err := fe.insertCart(r.Context(), sessionID(r), p.GetId(), int32(quantity)); err != nil { renderHTTPError(log, r, w, errors.Wrap(err, "failed to add to cart"), http.StatusInternalServerError) return @@ -297,6 +321,17 @@ func (fe *frontendServer) viewCartHandler(w http.ResponseWriter, r *http.Request totalPrice = money.Must(money.Sum(totalPrice, shippingCost)) year := time.Now().Year() + // add cart details to span as a manually created attributes + shippingCostFloat, _ := strconv.ParseFloat(fmt.Sprintf("%d.%02d", shippingCost.GetUnits(), shippingCost.GetNanos()/10000000), 64) + totalPriceFloat, _ := strconv.ParseFloat(fmt.Sprintf("%d.%02d", totalPrice.GetUnits(), totalPrice.GetNanos()/10000000), 64) + span := trace.SpanFromContext(r.Context()) + span.SetAttributes( + attribute.Int(instr.AppPrefix+"cart.size", cartSize(cart)), + attribute.Int(instr.AppPrefix+"cart.items.count", len(items)), + attribute.Float64(instr.AppPrefix+"cart.shipping.cost", shippingCostFloat), + attribute.Float64(instr.AppPrefix+"cart.total.price", totalPriceFloat), + ) + if err := templates.ExecuteTemplate(w, "cart", map[string]interface{}{ "session_id": sessionID(r), "request_id": r.Context().Value(ctxKeyRequestID{}), @@ -314,6 +349,7 @@ func (fe *frontendServer) viewCartHandler(w http.ResponseWriter, r *http.Request "is_cymbal_brand": isCymbalBrand, "deploymentDetails": deploymentDetailsMap, }); err != nil { + span.SetStatus(codes.Error, err.Error()) log.Println(err) } } @@ -367,6 +403,11 @@ func (fe *frontendServer) placeOrderHandler(w http.ResponseWriter, r *http.Reque totalPaid = money.Must(money.Sum(totalPaid, multPrice)) } + // add Order total paid to span as a manually created attribute + totalPaidFloat, _ := strconv.ParseFloat(fmt.Sprintf("%d.%02d", totalPaid.GetUnits(), totalPaid.GetNanos()/10000000), 64) + span := trace.SpanFromContext(r.Context()) + span.SetAttributes(attribute.Float64(instr.AppPrefix+"order.total", totalPaidFloat)) + currencies, err := fe.getCurrencies(r.Context()) if err != nil { renderHTTPError(log, r, w, errors.Wrap(err, "could not retrieve currencies"), http.StatusInternalServerError) @@ -387,6 +428,7 @@ func (fe *frontendServer) placeOrderHandler(w http.ResponseWriter, r *http.Reque "is_cymbal_brand": isCymbalBrand, "deploymentDetails": deploymentDetailsMap, }); err != nil { + span.SetStatus(codes.Error, err.Error()) log.Println(err) } } @@ -410,6 +452,8 @@ func (fe *frontendServer) setCurrencyHandler(w http.ResponseWriter, r *http.Requ Debug("setting currency") if cur != "" { + span := trace.SpanFromContext(r.Context()) + span.SetAttributes(attribute.String(instr.AppPrefix+"currency.new", cur)) http.SetCookie(w, &http.Cookie{ Name: cookieCurrency, Value: cur, @@ -439,6 +483,10 @@ func renderHTTPError(log logrus.FieldLogger, r *http.Request, w http.ResponseWri log.WithField("error", err).Error("request error") errMsg := fmt.Sprintf("%+v", err) + // set span status on error + span := trace.SpanFromContext(r.Context()) + span.SetStatus(codes.Error, errMsg) + w.WriteHeader(code) if templateErr := templates.ExecuteTemplate(w, "error", map[string]interface{}{ diff --git a/src/frontend/instr/conventions.go b/src/frontend/instr/conventions.go new file mode 100644 index 0000000000..617465e3c9 --- /dev/null +++ b/src/frontend/instr/conventions.go @@ -0,0 +1,13 @@ +package instr + +import "go.opentelemetry.io/otel/attribute" + +const AppPrefix = "app." + +const ( + SessionId = attribute.Key(AppPrefix + "session.id") + RequestId = attribute.Key(AppPrefix + "request.id") + UserId = attribute.Key(AppPrefix + "user.id") + + Currency = attribute.Key(AppPrefix + "currency") +) diff --git a/src/frontend/main.go b/src/frontend/main.go index 9666decd08..865514f051 100644 --- a/src/frontend/main.go +++ b/src/frontend/main.go @@ -130,14 +130,14 @@ func main() { r := mux.NewRouter() r.Use(otelmux.Middleware("server")) - 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.HandleFunc("/", instrumentHandler(svc.homeHandler)).Methods(http.MethodGet, http.MethodHead) + r.HandleFunc("/product/{id}", instrumentHandler(svc.productHandler)).Methods(http.MethodGet, http.MethodHead) + r.HandleFunc("/cart", instrumentHandler(svc.viewCartHandler)).Methods(http.MethodGet, http.MethodHead) + r.HandleFunc("/cart", instrumentHandler(svc.addToCartHandler)).Methods(http.MethodPost) + r.HandleFunc("/cart/empty", instrumentHandler(svc.emptyCartHandler)).Methods(http.MethodPost) + r.HandleFunc("/setCurrency", instrumentHandler(svc.setCurrencyHandler)).Methods(http.MethodPost) + r.HandleFunc("/logout", instrumentHandler(svc.logoutHandler)).Methods(http.MethodGet) + r.HandleFunc("/cart/checkout", instrumentHandler(svc.placeOrderHandler)).Methods(http.MethodPost) r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("./static/")))) r.HandleFunc("/robots.txt", func(w http.ResponseWriter, _ *http.Request) { fmt.Fprint(w, "User-agent: *\nDisallow: /") }) r.HandleFunc("/_healthz", func(w http.ResponseWriter, _ *http.Request) { fmt.Fprint(w, "ok") }) diff --git a/src/frontend/middleware.go b/src/frontend/middleware.go index 4d4221a5f7..7aeb2e3da1 100644 --- a/src/frontend/middleware.go +++ b/src/frontend/middleware.go @@ -16,6 +16,8 @@ package main import ( "context" + "github.com/GoogleCloudPlatform/microservices-demo/src/frontend/instr" + "go.opentelemetry.io/otel/trace" "net/http" "time" @@ -26,6 +28,8 @@ import ( type ctxKeyLog struct{} type ctxKeyRequestID struct{} +type httpHandler func(w http.ResponseWriter, r *http.Request) + type logHandler struct { log *logrus.Logger next http.Handler @@ -81,6 +85,32 @@ func (lh *logHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { lh.next.ServeHTTP(rr, r) } +func instrumentHandler(fn httpHandler) httpHandler { + // Add common attributes to the span for each handler + // session, request, currency, and user + + return func(w http.ResponseWriter, r *http.Request) { + rid := r.Context().Value(ctxKeyRequestID{}) + requestID := "" + if rid != nil { + requestID = rid.(string) + } + span := trace.SpanFromContext(r.Context()) + span.SetAttributes( + instr.SessionId.String(sessionID(r)), + instr.RequestId.String(requestID), + instr.Currency.String(currentCurrency(r)), + ) + + email := r.FormValue("email") + if email != "" { + span.SetAttributes(instr.UserId.String(email)) + } + + fn(w, r) + } +} + func ensureSessionID(next http.Handler) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var sessionID string