From 9cb7660b652c5e357e8fd5da6ddc0c93a9f3bedd Mon Sep 17 00:00:00 2001 From: Han Joker <540090808@qq.com> Date: Sat, 16 Mar 2024 21:24:02 +0800 Subject: [PATCH] all --- .gitignore | 1 + Dockerfile | 6 + cmd/all.go | 38 +++ cmd/customer.go | 49 ++++ cmd/driver.go | 49 ++++ cmd/empty_test.go | 25 ++ cmd/flags.go | 63 +++++ cmd/frontend.go | 58 ++++ cmd/root.go | 122 +++++++++ cmd/route.go | 49 ++++ docker-compose.yml | 33 +++ go.mod | 53 ++++ go.sum | 180 +++++++++++++ kubernetes/README.md | 17 ++ kubernetes/base/hotrod/deployment.yaml | 40 +++ kubernetes/base/hotrod/kustomization.yaml | 6 + kubernetes/base/hotrod/service.yaml | 12 + .../base/jaeger-all-in-one/kustomization.yaml | 33 +++ .../base/jaeger-all-in-one/service.yaml | 16 ++ kubernetes/kustomization.yaml | 13 + kubernetes/namespace.yaml | 4 + main.go | 12 + pkg/delay/delay.go | 31 +++ pkg/delay/empty_test.go | 25 ++ pkg/httperr/empty_test.go | 25 ++ pkg/httperr/httperr.go | 30 +++ pkg/log/empty_test.go | 25 ++ pkg/log/factory.go | 60 +++++ pkg/log/logger.go | 60 +++++ pkg/log/spanlogger.go | 176 ++++++++++++ pkg/pool/empty_test.go | 25 ++ pkg/pool/pool.go | 56 ++++ pkg/tracing/baggage.go | 27 ++ pkg/tracing/empty_test.go | 25 ++ pkg/tracing/http.go | 72 +++++ pkg/tracing/init.go | 111 ++++++++ pkg/tracing/mutex.go | 86 ++++++ pkg/tracing/mux.go | 77 ++++++ pkg/tracing/rpcmetrics/README.md | 3 + pkg/tracing/rpcmetrics/endpoints.go | 63 +++++ pkg/tracing/rpcmetrics/endpoints_test.go | 44 +++ pkg/tracing/rpcmetrics/metrics.go | 126 +++++++++ pkg/tracing/rpcmetrics/metrics_test.go | 62 +++++ pkg/tracing/rpcmetrics/normalizer.go | 101 +++++++ pkg/tracing/rpcmetrics/normalizer_test.go | 35 +++ pkg/tracing/rpcmetrics/observer.go | 92 +++++++ pkg/tracing/rpcmetrics/observer_test.go | 149 ++++++++++ pkg/tracing/rpcmetrics/package_test.go | 25 ++ services/config/config.go | 63 +++++ services/config/empty_test.go | 25 ++ services/customer/client.go | 55 ++++ services/customer/database.go | 100 +++++++ services/customer/empty_test.go | 25 ++ services/customer/interface.go | 32 +++ services/customer/server.go | 105 ++++++++ services/driver/client.go | 75 ++++++ services/driver/driver.pb.go | 254 ++++++++++++++++++ services/driver/driver.proto | 35 +++ services/driver/empty_test.go | 25 ++ services/driver/interface.go | 31 +++ services/driver/redis.go | 112 ++++++++ services/driver/server.go | 110 ++++++++ services/frontend/best_eta.go | 147 ++++++++++ services/frontend/empty_test.go | 25 ++ services/frontend/server.go | 143 ++++++++++ .../code.jquery.com_jquery-3.7.0.min.js | 2 + services/frontend/web_assets/favicon.ico | Bin 0 -> 15406 bytes services/frontend/web_assets/index.html | 131 +++++++++ services/route/client.go | 59 ++++ services/route/empty_test.go | 25 ++ services/route/interface.go | 33 +++ services/route/server.go | 123 +++++++++ services/route/stats.go | 53 ++++ 73 files changed, 4278 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 cmd/all.go create mode 100644 cmd/customer.go create mode 100644 cmd/driver.go create mode 100644 cmd/empty_test.go create mode 100644 cmd/flags.go create mode 100644 cmd/frontend.go create mode 100644 cmd/root.go create mode 100644 cmd/route.go create mode 100644 docker-compose.yml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 kubernetes/README.md create mode 100644 kubernetes/base/hotrod/deployment.yaml create mode 100644 kubernetes/base/hotrod/kustomization.yaml create mode 100644 kubernetes/base/hotrod/service.yaml create mode 100644 kubernetes/base/jaeger-all-in-one/kustomization.yaml create mode 100644 kubernetes/base/jaeger-all-in-one/service.yaml create mode 100644 kubernetes/kustomization.yaml create mode 100644 kubernetes/namespace.yaml create mode 100644 main.go create mode 100644 pkg/delay/delay.go create mode 100644 pkg/delay/empty_test.go create mode 100644 pkg/httperr/empty_test.go create mode 100644 pkg/httperr/httperr.go create mode 100644 pkg/log/empty_test.go create mode 100644 pkg/log/factory.go create mode 100644 pkg/log/logger.go create mode 100644 pkg/log/spanlogger.go create mode 100644 pkg/pool/empty_test.go create mode 100644 pkg/pool/pool.go create mode 100644 pkg/tracing/baggage.go create mode 100644 pkg/tracing/empty_test.go create mode 100644 pkg/tracing/http.go create mode 100644 pkg/tracing/init.go create mode 100644 pkg/tracing/mutex.go create mode 100644 pkg/tracing/mux.go create mode 100644 pkg/tracing/rpcmetrics/README.md create mode 100644 pkg/tracing/rpcmetrics/endpoints.go create mode 100644 pkg/tracing/rpcmetrics/endpoints_test.go create mode 100644 pkg/tracing/rpcmetrics/metrics.go create mode 100644 pkg/tracing/rpcmetrics/metrics_test.go create mode 100644 pkg/tracing/rpcmetrics/normalizer.go create mode 100644 pkg/tracing/rpcmetrics/normalizer_test.go create mode 100644 pkg/tracing/rpcmetrics/observer.go create mode 100644 pkg/tracing/rpcmetrics/observer_test.go create mode 100644 pkg/tracing/rpcmetrics/package_test.go create mode 100644 services/config/config.go create mode 100644 services/config/empty_test.go create mode 100644 services/customer/client.go create mode 100644 services/customer/database.go create mode 100644 services/customer/empty_test.go create mode 100644 services/customer/interface.go create mode 100644 services/customer/server.go create mode 100644 services/driver/client.go create mode 100644 services/driver/driver.pb.go create mode 100644 services/driver/driver.proto create mode 100644 services/driver/empty_test.go create mode 100644 services/driver/interface.go create mode 100644 services/driver/redis.go create mode 100644 services/driver/server.go create mode 100644 services/frontend/best_eta.go create mode 100644 services/frontend/empty_test.go create mode 100644 services/frontend/server.go create mode 100644 services/frontend/web_assets/code.jquery.com_jquery-3.7.0.min.js create mode 100644 services/frontend/web_assets/favicon.ico create mode 100644 services/frontend/web_assets/index.html create mode 100644 services/route/client.go create mode 100644 services/route/empty_test.go create mode 100644 services/route/interface.go create mode 100644 services/route/server.go create mode 100644 services/route/stats.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ab92e1f --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +hotrod-linux diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b93de0e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,6 @@ +FROM scratch +ARG TARGETARCH +EXPOSE 8080 8081 8082 8083 +COPY hotrod-linux-$TARGETARCH /go/bin/hotrod-linux +ENTRYPOINT ["/go/bin/hotrod-linux"] +CMD ["all"] diff --git a/cmd/all.go b/cmd/all.go new file mode 100644 index 0000000..a64b407 --- /dev/null +++ b/cmd/all.go @@ -0,0 +1,38 @@ +// Copyright (c) 2019 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "github.com/spf13/cobra" +) + +// allCmd represents the all command +var allCmd = &cobra.Command{ + Use: "all", + Short: "Starts all services", + Long: `Starts all services.`, + RunE: func(cmd *cobra.Command, args []string) error { + logger.Info("Starting all services") + go customerCmd.RunE(customerCmd, args) + go driverCmd.RunE(driverCmd, args) + go routeCmd.RunE(routeCmd, args) + return frontendCmd.RunE(frontendCmd, args) + }, +} + +func init() { + RootCmd.AddCommand(allCmd) +} diff --git a/cmd/customer.go b/cmd/customer.go new file mode 100644 index 0000000..9952b2c --- /dev/null +++ b/cmd/customer.go @@ -0,0 +1,49 @@ +// Copyright (c) 2019 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "net" + "strconv" + + "github.com/spf13/cobra" + "go.uber.org/zap" + + "github.com/jaegertracing/jaeger/examples/hotrod/pkg/log" + "github.com/jaegertracing/jaeger/examples/hotrod/services/customer" +) + +// customerCmd represents the customer command +var customerCmd = &cobra.Command{ + Use: "customer", + Short: "Starts Customer service", + Long: `Starts Customer service.`, + RunE: func(cmd *cobra.Command, args []string) error { + zapLogger := logger.With(zap.String("service", "customer")) + logger := log.NewFactory(zapLogger) + server := customer.NewServer( + net.JoinHostPort("0.0.0.0", strconv.Itoa(customerPort)), + otelExporter, + metricsFactory, + logger, + ) + return logError(zapLogger, server.Run()) + }, +} + +func init() { + RootCmd.AddCommand(customerCmd) +} diff --git a/cmd/driver.go b/cmd/driver.go new file mode 100644 index 0000000..5ff98d8 --- /dev/null +++ b/cmd/driver.go @@ -0,0 +1,49 @@ +// Copyright (c) 2019 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "net" + "strconv" + + "github.com/spf13/cobra" + "go.uber.org/zap" + + "github.com/jaegertracing/jaeger/examples/hotrod/pkg/log" + "github.com/jaegertracing/jaeger/examples/hotrod/services/driver" +) + +// driverCmd represents the driver command +var driverCmd = &cobra.Command{ + Use: "driver", + Short: "Starts Driver service", + Long: `Starts Driver service.`, + RunE: func(cmd *cobra.Command, args []string) error { + zapLogger := logger.With(zap.String("service", "driver")) + logger := log.NewFactory(zapLogger) + server := driver.NewServer( + net.JoinHostPort("0.0.0.0", strconv.Itoa(driverPort)), + otelExporter, + metricsFactory, + logger, + ) + return logError(zapLogger, server.Run()) + }, +} + +func init() { + RootCmd.AddCommand(driverCmd) +} diff --git a/cmd/empty_test.go b/cmd/empty_test.go new file mode 100644 index 0000000..febd67c --- /dev/null +++ b/cmd/empty_test.go @@ -0,0 +1,25 @@ +// Copyright (c) 2024 The Jaeger Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "testing" + + "github.com/jaegertracing/jaeger/pkg/testutils" +) + +func TestMain(m *testing.M) { + testutils.VerifyGoLeaks(m) +} diff --git a/cmd/flags.go b/cmd/flags.go new file mode 100644 index 0000000..c09403f --- /dev/null +++ b/cmd/flags.go @@ -0,0 +1,63 @@ +// Copyright (c) 2023 The Jaeger Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "time" + + "github.com/spf13/cobra" +) + +var ( + metricsBackend string + otelExporter string // otlp, stdout + verbose bool + + fixDBConnDelay time.Duration + fixDBConnDisableMutex bool + fixRouteWorkerPoolSize int + + customerPort int + driverPort int + frontendPort int + routePort int + + basepath string + jaegerUI string +) + +const expvarDepr = "(deprecated, will be removed after 2024-01-01 or in release v1.53.0, whichever is later) " + +// used by root command +func addFlags(cmd *cobra.Command) { + cmd.PersistentFlags().StringVarP(&metricsBackend, "metrics", "m", "prometheus", expvarDepr+"Metrics backend (expvar|prometheus). ") + cmd.PersistentFlags().StringVarP(&otelExporter, "otel-exporter", "x", "otlp", "OpenTelemetry exporter (otlp|stdout)") + + cmd.PersistentFlags().DurationVarP(&fixDBConnDelay, "fix-db-query-delay", "D", 300*time.Millisecond, "Average latency of MySQL DB query") + cmd.PersistentFlags().BoolVarP(&fixDBConnDisableMutex, "fix-disable-db-conn-mutex", "M", false, "Disables the mutex guarding db connection") + cmd.PersistentFlags().IntVarP(&fixRouteWorkerPoolSize, "fix-route-worker-pool-size", "W", 3, "Default worker pool size") + + // Add flags to choose ports for services + cmd.PersistentFlags().IntVarP(&customerPort, "customer-service-port", "c", 8081, "Port for customer service") + cmd.PersistentFlags().IntVarP(&driverPort, "driver-service-port", "d", 8082, "Port for driver service") + cmd.PersistentFlags().IntVarP(&frontendPort, "frontend-service-port", "f", 8080, "Port for frontend service") + cmd.PersistentFlags().IntVarP(&routePort, "route-service-port", "r", 8083, "Port for routing service") + + // Flag for serving frontend at custom basepath url + cmd.PersistentFlags().StringVarP(&basepath, "basepath", "b", "", `Basepath for frontend service(default "/")`) + cmd.PersistentFlags().StringVarP(&jaegerUI, "jaeger-ui", "j", "http://localhost:16686", "Address of Jaeger UI to create [find trace] links") + + cmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Enables debug logging") +} diff --git a/cmd/frontend.go b/cmd/frontend.go new file mode 100644 index 0000000..4f17642 --- /dev/null +++ b/cmd/frontend.go @@ -0,0 +1,58 @@ +// Copyright (c) 2019 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "net" + "strconv" + + "github.com/spf13/cobra" + "go.uber.org/zap" + + "github.com/jaegertracing/jaeger/examples/hotrod/pkg/log" + "github.com/jaegertracing/jaeger/examples/hotrod/pkg/tracing" + "github.com/jaegertracing/jaeger/examples/hotrod/services/frontend" +) + +// frontendCmd represents the frontend command +var frontendCmd = &cobra.Command{ + Use: "frontend", + Short: "Starts Frontend service", + Long: `Starts Frontend service.`, + RunE: func(cmd *cobra.Command, args []string) error { + options.FrontendHostPort = net.JoinHostPort("0.0.0.0", strconv.Itoa(frontendPort)) + options.DriverHostPort = net.JoinHostPort("0.0.0.0", strconv.Itoa(driverPort)) + options.CustomerHostPort = net.JoinHostPort("0.0.0.0", strconv.Itoa(customerPort)) + options.RouteHostPort = net.JoinHostPort("0.0.0.0", strconv.Itoa(routePort)) + options.Basepath = basepath + options.JaegerUI = jaegerUI + + zapLogger := logger.With(zap.String("service", "frontend")) + logger := log.NewFactory(zapLogger) + server := frontend.NewServer( + options, + tracing.InitOTEL("frontend", otelExporter, metricsFactory, logger), + logger, + ) + return logError(zapLogger, server.Run()) + }, +} + +var options frontend.ConfigOptions + +func init() { + RootCmd.AddCommand(frontendCmd) +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..530fe83 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,122 @@ +// Copyright (c) 2019 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "os" + + "github.com/spf13/cobra" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + + "github.com/jaegertracing/jaeger/examples/hotrod/services/config" + "github.com/jaegertracing/jaeger/internal/jaegerclientenv2otel" + "github.com/jaegertracing/jaeger/internal/metrics/expvar" + "github.com/jaegertracing/jaeger/internal/metrics/prometheus" + "github.com/jaegertracing/jaeger/pkg/metrics" +) + +var ( + logger *zap.Logger + metricsFactory metrics.Factory +) + +// RootCmd represents the base command when called without any subcommands +var RootCmd = &cobra.Command{ + Use: "examples-hotrod", + Short: "HotR.O.D. - A tracing demo application", + Long: `HotR.O.D. - A tracing demo application.`, +} + +// Execute adds all child commands to the root command sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + if err := RootCmd.Execute(); err != nil { + logger.Fatal("We bowled a googly", zap.Error(err)) + os.Exit(-1) + } +} + +func init() { + addFlags(RootCmd) + cobra.OnInitialize(onInitialize) +} + +// onInitialize is called before the command is executed. +func onInitialize() { + zapOptions := []zap.Option{ + zap.AddStacktrace(zapcore.FatalLevel), + zap.AddCallerSkip(1), + } + if !verbose { + zapOptions = append(zapOptions, + zap.IncreaseLevel(zap.LevelEnablerFunc(func(l zapcore.Level) bool { return l != zapcore.DebugLevel })), + ) + } + logger, _ = zap.NewDevelopment(zapOptions...) + + jaegerclientenv2otel.MapJaegerToOtelEnvVars(logger) + + switch metricsBackend { + case "expvar": + metricsFactory = expvar.NewFactory(10) // 10 buckets for histograms + logger.Info("*** Using expvar as metrics backend " + expvarDepr) + case "prometheus": + metricsFactory = prometheus.New().Namespace(metrics.NSOptions{Name: "hotrod", Tags: nil}) + logger.Info("Using Prometheus as metrics backend") + default: + logger.Fatal("unsupported metrics backend " + metricsBackend) + } + if config.MySQLGetDelay != fixDBConnDelay { + logger.Info("fix: overriding MySQL query delay", zap.Duration("old", config.MySQLGetDelay), zap.Duration("new", fixDBConnDelay)) + config.MySQLGetDelay = fixDBConnDelay + } + if fixDBConnDisableMutex { + logger.Info("fix: disabling db connection mutex") + config.MySQLMutexDisabled = true + } + if config.RouteWorkerPoolSize != fixRouteWorkerPoolSize { + logger.Info("fix: overriding route worker pool size", zap.Int("old", config.RouteWorkerPoolSize), zap.Int("new", fixRouteWorkerPoolSize)) + config.RouteWorkerPoolSize = fixRouteWorkerPoolSize + } + + if customerPort != 8081 { + logger.Info("changing customer service port", zap.Int("old", 8081), zap.Int("new", customerPort)) + } + + if driverPort != 8082 { + logger.Info("changing driver service port", zap.Int("old", 8082), zap.Int("new", driverPort)) + } + + if frontendPort != 8080 { + logger.Info("changing frontend service port", zap.Int("old", 8080), zap.Int("new", frontendPort)) + } + + if routePort != 8083 { + logger.Info("changing route service port", zap.Int("old", 8083), zap.Int("new", routePort)) + } + + if basepath != "" { + logger.Info("changing basepath for frontend", zap.String("old", "/"), zap.String("new", basepath)) + } +} + +func logError(logger *zap.Logger, err error) error { + if err != nil { + logger.Error("Error running command", zap.Error(err)) + } + return err +} diff --git a/cmd/route.go b/cmd/route.go new file mode 100644 index 0000000..04cf368 --- /dev/null +++ b/cmd/route.go @@ -0,0 +1,49 @@ +// Copyright (c) 2019 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "net" + "strconv" + + "github.com/spf13/cobra" + "go.uber.org/zap" + + "github.com/jaegertracing/jaeger/examples/hotrod/pkg/log" + "github.com/jaegertracing/jaeger/examples/hotrod/pkg/tracing" + "github.com/jaegertracing/jaeger/examples/hotrod/services/route" +) + +// routeCmd represents the route command +var routeCmd = &cobra.Command{ + Use: "route", + Short: "Starts Route service", + Long: `Starts Route service.`, + RunE: func(cmd *cobra.Command, args []string) error { + zapLogger := logger.With(zap.String("service", "route")) + logger := log.NewFactory(zapLogger) + server := route.NewServer( + net.JoinHostPort("0.0.0.0", strconv.Itoa(routePort)), + tracing.InitOTEL("route", otelExporter, metricsFactory, logger), + logger, + ) + return logError(zapLogger, server.Run()) + }, +} + +func init() { + RootCmd.AddCommand(routeCmd) +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..db58cb2 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,33 @@ +version: '3.7' + +# To run a specific version of Jaeger, use environment variable, e.g.: +# JAEGER_VERSION=1.52 docker compose up + +services: + jaeger: + image: jaegertracing/all-in-one:${JAEGER_VERSION:-latest} + ports: + - "16686:16686" + - "4318:4318" + environment: + - LOG_LEVEL=debug + networks: + - jaeger-example + hotrod: + image: jaegertracing/example-hotrod:${JAEGER_VERSION:-latest} + # To run the latest trunk build, find the tag at Docker Hub and use the line below + # https://hub.docker.com/r/jaegertracing/example-hotrod-snapshot/tags + #image: jaegertracing/example-hotrod-snapshot:0ab8f2fcb12ff0d10830c1ee3bb52b745522db6c + ports: + - "8080:8080" + - "8083:8083" + command: ["all"] + environment: + - OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318 + networks: + - jaeger-example + depends_on: + - jaeger + +networks: + jaeger-example: diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..187a43d --- /dev/null +++ b/go.mod @@ -0,0 +1,53 @@ +module hotrod + +go 1.22.0 + +require ( + github.com/gogo/protobuf v1.3.2 + github.com/jaegertracing/jaeger v1.55.0 + github.com/prometheus/client_golang v1.19.0 + github.com/spf13/cobra v1.8.0 + github.com/stretchr/testify v1.9.0 + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 + go.opentelemetry.io/otel v1.24.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 + go.opentelemetry.io/otel/sdk v1.24.0 + go.opentelemetry.io/otel/trace v1.24.0 + go.uber.org/zap v1.27.0 + google.golang.org/grpc v1.62.1 +) + +require ( + github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect + github.com/VividCortex/gohistogram v1.0.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-kit/kit v0.13.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_model v0.6.0 // indirect + github.com/prometheus/common v0.49.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + go.opentelemetry.io/otel/metric v1.24.0 // indirect + go.opentelemetry.io/proto/otlp v1.1.0 // indirect + go.uber.org/goleak v1.3.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect + google.golang.org/protobuf v1.32.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3740e86 --- /dev/null +++ b/go.sum @@ -0,0 +1,180 @@ +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= +github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= +github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU= +github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jaegertracing/jaeger v1.55.0 h1:IJHzKb2B9EYQyKlE7VSoKzNP3emHeqZWnWrKj+kYzzs= +github.com/jaegertracing/jaeger v1.55.0/go.mod h1:S884Mz8H+iGI8Ealq6sM9QzSOeU6P+nbFkYw7uww8CI= +github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= +github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= +github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos= +github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8= +github.com/prometheus/common v0.49.0 h1:ToNTdK4zSnPVJmh698mGFkDor9wBI/iGaJy5dbH1EgI= +github.com/prometheus/common v0.49.0/go.mod h1:Kxm+EULxRbUkjGU6WFsQqo3ORzB4tyKvlWFOE9mB2sE= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 h1:s0PHtIkN+3xrbDOpt2M8OTG92cWqUESvzh2MxiR5xY8= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0/go.mod h1:hZlFbDbRt++MMPCCfSJfmhkGIWnX1h3XjkfxZUjLrIA= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= +go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI= +go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc h1:ao2WRsKSzW6KuUY9IWPwWahcHCgR0s52IfwutMfEbdM= +golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= +gonum.org/v1/gonum v0.14.0 h1:2NiG67LD1tEH0D7kM+ps2V+fXmsAnpUeec7n8tcr4S0= +gonum.org/v1/gonum v0.14.0/go.mod h1:AoWeoz0becf9QMWtE8iWXNXc27fK4fNeHNf/oMejGfU= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= +google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ= +google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80/go.mod h1:cc8bqMqtv9gMOr0zHg2Vzff5ULhhL2IXP4sbcn32Dro= +google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 h1:Lj5rbfG876hIAYFjqiJnPHfhXbv+nzTWfm04Fg/XSVU= +google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 h1:AjyfHzEPEFp/NpvfN5g+KDla3EMojjhRVZc1i7cj+oM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s= +google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk= +google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/kubernetes/README.md b/kubernetes/README.md new file mode 100644 index 0000000..e58aa2f --- /dev/null +++ b/kubernetes/README.md @@ -0,0 +1,17 @@ +# Hot R.O.D. - Rides on Demand on Kubernetes + +Example k8s manifests for deploying the [hotrod app](..) to your k8s environment of choice. e.g. minikube, k3s, EKS, GKE + +## Usage + +```bash +kustomize build . | kubectl apply -f - +kubectl port-forward -n example-hotrod service/example-hotrod 8080:frontend +# In another terminal +kubectl port-forward -n example-hotrod service/jaeger 16686:frontend + +# To cleanup +kustomize build . | kubectl delete -f - +``` + +Access Jaeger UI at and HotROD app at diff --git a/kubernetes/base/hotrod/deployment.yaml b/kubernetes/base/hotrod/deployment.yaml new file mode 100644 index 0000000..420f531 --- /dev/null +++ b/kubernetes/base/hotrod/deployment.yaml @@ -0,0 +1,40 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: example-hotrod + name: example-hotrod +spec: + replicas: 1 + selector: + matchLabels: + app: example-hotrod + strategy: {} + template: + metadata: + labels: + app: example-hotrod + spec: + containers: + - image: jaegertracing/example-hotrod:latest + name: example-hotrod + args: ["all"] + env: + - name: JAEGER_AGENT_HOST + value: localhost + - name: JAEGER_AGENT_PORT + value: "6831" + ports: + - containerPort: 8080 + name: frontend + - containerPort: 8081 + name: customer + - containerPort: 8083 + name: route + resources: + limits: + cpu: 100m + memory: 100M + requests: + cpu: 100m + memory: 100M diff --git a/kubernetes/base/hotrod/kustomization.yaml b/kubernetes/base/hotrod/kustomization.yaml new file mode 100644 index 0000000..5b98e94 --- /dev/null +++ b/kubernetes/base/hotrod/kustomization.yaml @@ -0,0 +1,6 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - deployment.yaml + - service.yaml diff --git a/kubernetes/base/hotrod/service.yaml b/kubernetes/base/hotrod/service.yaml new file mode 100644 index 0000000..7ac8a5c --- /dev/null +++ b/kubernetes/base/hotrod/service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: example-hotrod +spec: + selector: + app: example-hotrod + ports: + - name: frontend + protocol: TCP + port: 8080 + targetPort: frontend diff --git a/kubernetes/base/jaeger-all-in-one/kustomization.yaml b/kubernetes/base/jaeger-all-in-one/kustomization.yaml new file mode 100644 index 0000000..e64e7bb --- /dev/null +++ b/kubernetes/base/jaeger-all-in-one/kustomization.yaml @@ -0,0 +1,33 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +bases: + - ../hotrod + +resources: + - service.yaml + +patches: + - target: + group: apps + version: v1 + kind: Deployment + name: example-hotrod + patch: |- + - op: add + path: /spec/template/spec/containers/- + value: + image: jaegertracing/all-in-one:latest + name: jaeger + ports: + - containerPort: 6831 + name: tracing-jaeger + - containerPort: 16686 + name: frontend-jaeger + resources: + limits: + cpu: 100m + memory: 100M + requests: + cpu: 100m + memory: 100M diff --git a/kubernetes/base/jaeger-all-in-one/service.yaml b/kubernetes/base/jaeger-all-in-one/service.yaml new file mode 100644 index 0000000..e893304 --- /dev/null +++ b/kubernetes/base/jaeger-all-in-one/service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: jaeger +spec: + selector: + app: example-hotrod + ports: + - name: tracing + protocol: UDP + port: 6831 + targetPort: tracing-jaeger + - name: frontend + protocol: TCP + port: 16686 + targetPort: frontend-jaeger diff --git a/kubernetes/kustomization.yaml b/kubernetes/kustomization.yaml new file mode 100644 index 0000000..9614a74 --- /dev/null +++ b/kubernetes/kustomization.yaml @@ -0,0 +1,13 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +bases: +# If you only want the hotrod application, uncomment this line and comment out jaeger-all-in-one +# - base/hotrod +# Leaving this uncommented will install both hotrod and an example all-in-one + - base/jaeger-all-in-one + +namespace: example-hotrod + +resources: + - namespace.yaml diff --git a/kubernetes/namespace.yaml b/kubernetes/namespace.yaml new file mode 100644 index 0000000..e89d2e0 --- /dev/null +++ b/kubernetes/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: example-hotrod diff --git a/main.go b/main.go new file mode 100644 index 0000000..f4fca45 --- /dev/null +++ b/main.go @@ -0,0 +1,12 @@ +// Copyright (c) 2019 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. + +package main + +import ( + "github.com/jaegertracing/jaeger/examples/hotrod/cmd" +) + +func main() { + cmd.Execute() +} diff --git a/pkg/delay/delay.go b/pkg/delay/delay.go new file mode 100644 index 0000000..d797a68 --- /dev/null +++ b/pkg/delay/delay.go @@ -0,0 +1,31 @@ +// Copyright (c) 2019 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package delay + +import ( + "math" + "math/rand" + "time" +) + +// Sleep generates a normally distributed random delay with given mean and stdDev +// and blocks for that duration. +func Sleep(mean time.Duration, stdDev time.Duration) { + fMean := float64(mean) + fStdDev := float64(stdDev) + delay := time.Duration(math.Max(1, rand.NormFloat64()*fStdDev+fMean)) + time.Sleep(delay) +} diff --git a/pkg/delay/empty_test.go b/pkg/delay/empty_test.go new file mode 100644 index 0000000..54f880a --- /dev/null +++ b/pkg/delay/empty_test.go @@ -0,0 +1,25 @@ +// Copyright (c) 2024 The Jaeger Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package delay + +import ( + "testing" + + "github.com/jaegertracing/jaeger/pkg/testutils" +) + +func TestMain(m *testing.M) { + testutils.VerifyGoLeaks(m) +} diff --git a/pkg/httperr/empty_test.go b/pkg/httperr/empty_test.go new file mode 100644 index 0000000..df44d98 --- /dev/null +++ b/pkg/httperr/empty_test.go @@ -0,0 +1,25 @@ +// Copyright (c) 2024 The Jaeger Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package httperr + +import ( + "testing" + + "github.com/jaegertracing/jaeger/pkg/testutils" +) + +func TestMain(m *testing.M) { + testutils.VerifyGoLeaks(m) +} diff --git a/pkg/httperr/httperr.go b/pkg/httperr/httperr.go new file mode 100644 index 0000000..d7ef283 --- /dev/null +++ b/pkg/httperr/httperr.go @@ -0,0 +1,30 @@ +// Copyright (c) 2019 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package httperr + +import ( + "net/http" +) + +// HandleError checks if the error is not nil, writes it to the output +// with the specified status code, and returns true. If error is nil it returns false. +func HandleError(w http.ResponseWriter, err error, statusCode int) bool { + if err == nil { + return false + } + http.Error(w, string(err.Error()), statusCode) + return true +} diff --git a/pkg/log/empty_test.go b/pkg/log/empty_test.go new file mode 100644 index 0000000..2382c53 --- /dev/null +++ b/pkg/log/empty_test.go @@ -0,0 +1,25 @@ +// Copyright (c) 2024 The Jaeger Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package log + +import ( + "testing" + + "github.com/jaegertracing/jaeger/pkg/testutils" +) + +func TestMain(m *testing.M) { + testutils.VerifyGoLeaks(m) +} diff --git a/pkg/log/factory.go b/pkg/log/factory.go new file mode 100644 index 0000000..7f77732 --- /dev/null +++ b/pkg/log/factory.go @@ -0,0 +1,60 @@ +// Copyright (c) 2019 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package log + +import ( + "context" + + "go.opentelemetry.io/otel/trace" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// Factory is the default logging wrapper that can create +// logger instances either for a given Context or context-less. +type Factory struct { + logger *zap.Logger +} + +// NewFactory creates a new Factory. +func NewFactory(logger *zap.Logger) Factory { + return Factory{logger: logger} +} + +// Bg creates a context-unaware logger. +func (b Factory) Bg() Logger { + return wrapper(b) +} + +// For returns a context-aware Logger. If the context +// contains a span, all logging calls are also +// echo-ed into the span. +func (b Factory) For(ctx context.Context) Logger { + if span := trace.SpanFromContext(ctx); span != nil { + logger := spanLogger{span: span, logger: b.logger} + logger.spanFields = []zapcore.Field{ + zap.String("trace_id", span.SpanContext().TraceID().String()), + zap.String("span_id", span.SpanContext().SpanID().String()), + } + return logger + } + return b.Bg() +} + +// With creates a child logger, and optionally adds some context fields to that logger. +func (b Factory) With(fields ...zapcore.Field) Factory { + return Factory{logger: b.logger.With(fields...)} +} diff --git a/pkg/log/logger.go b/pkg/log/logger.go new file mode 100644 index 0000000..b81c18a --- /dev/null +++ b/pkg/log/logger.go @@ -0,0 +1,60 @@ +// Copyright (c) 2019 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package log + +import ( + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// Logger is a simplified abstraction of the zap.Logger +type Logger interface { + Debug(msg string, fields ...zapcore.Field) + Info(msg string, fields ...zapcore.Field) + Error(msg string, fields ...zapcore.Field) + Fatal(msg string, fields ...zapcore.Field) + With(fields ...zapcore.Field) Logger +} + +// wrapper delegates all calls to the underlying zap.Logger +type wrapper struct { + logger *zap.Logger +} + +// Debug logs an debug msg with fields +func (l wrapper) Debug(msg string, fields ...zapcore.Field) { + l.logger.Debug(msg, fields...) +} + +// Info logs an info msg with fields +func (l wrapper) Info(msg string, fields ...zapcore.Field) { + l.logger.Info(msg, fields...) +} + +// Error logs an error msg with fields +func (l wrapper) Error(msg string, fields ...zapcore.Field) { + l.logger.Error(msg, fields...) +} + +// Fatal logs a fatal error msg with fields +func (l wrapper) Fatal(msg string, fields ...zapcore.Field) { + l.logger.Fatal(msg, fields...) +} + +// With creates a child logger, and optionally adds some context fields to that logger. +func (l wrapper) With(fields ...zapcore.Field) Logger { + return wrapper{logger: l.logger.With(fields...)} +} diff --git a/pkg/log/spanlogger.go b/pkg/log/spanlogger.go new file mode 100644 index 0000000..d8f6476 --- /dev/null +++ b/pkg/log/spanlogger.go @@ -0,0 +1,176 @@ +// Copyright (c) 2019 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package log + +import ( + "fmt" + "time" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +type spanLogger struct { + logger *zap.Logger + span trace.Span + spanFields []zapcore.Field +} + +func (sl spanLogger) Debug(msg string, fields ...zapcore.Field) { + sl.logToSpan("debug", msg, fields...) + sl.logger.Debug(msg, append(sl.spanFields, fields...)...) +} + +func (sl spanLogger) Info(msg string, fields ...zapcore.Field) { + sl.logToSpan("info", msg, fields...) + sl.logger.Info(msg, append(sl.spanFields, fields...)...) +} + +func (sl spanLogger) Error(msg string, fields ...zapcore.Field) { + sl.logToSpan("error", msg, fields...) + sl.logger.Error(msg, append(sl.spanFields, fields...)...) +} + +func (sl spanLogger) Fatal(msg string, fields ...zapcore.Field) { + sl.logToSpan("fatal", msg, fields...) + sl.span.SetStatus(codes.Error, msg) + sl.logger.Fatal(msg, append(sl.spanFields, fields...)...) +} + +// With creates a child logger, and optionally adds some context fields to that logger. +func (sl spanLogger) With(fields ...zapcore.Field) Logger { + return spanLogger{logger: sl.logger.With(fields...), span: sl.span, spanFields: sl.spanFields} +} + +func (sl spanLogger) logToSpan(level, msg string, fields ...zapcore.Field) { + fields = append(fields, zap.String("level", level)) + sl.span.AddEvent( + msg, + trace.WithAttributes(logFieldsToOTelAttrs(fields)...), + ) +} + +func logFieldsToOTelAttrs(fields []zapcore.Field) []attribute.KeyValue { + encoder := &bridgeFieldEncoder{} + for _, field := range fields { + field.AddTo(encoder) + } + return encoder.pairs +} + +type bridgeFieldEncoder struct { + pairs []attribute.KeyValue +} + +func (e *bridgeFieldEncoder) AddArray(key string, marshaler zapcore.ArrayMarshaler) error { + e.pairs = append(e.pairs, attribute.String(key, fmt.Sprint(marshaler))) + return nil +} + +func (e *bridgeFieldEncoder) AddObject(key string, marshaler zapcore.ObjectMarshaler) error { + e.pairs = append(e.pairs, attribute.String(key, fmt.Sprint(marshaler))) + return nil +} + +func (e *bridgeFieldEncoder) AddBinary(key string, value []byte) { + e.pairs = append(e.pairs, attribute.String(key, fmt.Sprint(value))) +} + +func (e *bridgeFieldEncoder) AddByteString(key string, value []byte) { + e.pairs = append(e.pairs, attribute.String(key, fmt.Sprint(value))) +} + +func (e *bridgeFieldEncoder) AddBool(key string, value bool) { + e.pairs = append(e.pairs, attribute.Bool(key, value)) +} + +func (e *bridgeFieldEncoder) AddComplex128(key string, value complex128) { + e.pairs = append(e.pairs, attribute.String(key, fmt.Sprint(value))) +} + +func (e *bridgeFieldEncoder) AddComplex64(key string, value complex64) { + e.pairs = append(e.pairs, attribute.String(key, fmt.Sprint(value))) +} + +func (e *bridgeFieldEncoder) AddDuration(key string, value time.Duration) { + e.pairs = append(e.pairs, attribute.String(key, fmt.Sprint(value))) +} + +func (e *bridgeFieldEncoder) AddFloat64(key string, value float64) { + e.pairs = append(e.pairs, attribute.Float64(key, value)) +} + +func (e *bridgeFieldEncoder) AddFloat32(key string, value float32) { + e.pairs = append(e.pairs, attribute.Float64(key, float64(value))) +} + +func (e *bridgeFieldEncoder) AddInt(key string, value int) { + e.pairs = append(e.pairs, attribute.Int(key, value)) +} + +func (e *bridgeFieldEncoder) AddInt64(key string, value int64) { + e.pairs = append(e.pairs, attribute.Int64(key, value)) +} + +func (e *bridgeFieldEncoder) AddInt32(key string, value int32) { + e.pairs = append(e.pairs, attribute.Int64(key, int64(value))) +} + +func (e *bridgeFieldEncoder) AddInt16(key string, value int16) { + e.pairs = append(e.pairs, attribute.Int64(key, int64(value))) +} + +func (e *bridgeFieldEncoder) AddInt8(key string, value int8) { + e.pairs = append(e.pairs, attribute.Int64(key, int64(value))) +} + +func (e *bridgeFieldEncoder) AddString(key, value string) { + e.pairs = append(e.pairs, attribute.String(key, value)) +} + +func (e *bridgeFieldEncoder) AddTime(key string, value time.Time) { + e.pairs = append(e.pairs, attribute.String(key, fmt.Sprint(value))) +} + +func (e *bridgeFieldEncoder) AddUint(key string, value uint) { + e.pairs = append(e.pairs, attribute.String(key, fmt.Sprintf("%d", value))) +} + +func (e *bridgeFieldEncoder) AddUint64(key string, value uint64) { + e.pairs = append(e.pairs, attribute.String(key, fmt.Sprintf("%d", value))) +} + +func (e *bridgeFieldEncoder) AddUint32(key string, value uint32) { + e.pairs = append(e.pairs, attribute.Int64(key, int64(value))) +} + +func (e *bridgeFieldEncoder) AddUint16(key string, value uint16) { + e.pairs = append(e.pairs, attribute.Int64(key, int64(value))) +} + +func (e *bridgeFieldEncoder) AddUint8(key string, value uint8) { + e.pairs = append(e.pairs, attribute.Int64(key, int64(value))) +} + +func (e *bridgeFieldEncoder) AddUintptr(key string, value uintptr) { + e.pairs = append(e.pairs, attribute.String(key, fmt.Sprint(value))) +} + +func (e *bridgeFieldEncoder) AddReflected(key string, value interface{}) error { return nil } +func (e *bridgeFieldEncoder) OpenNamespace(key string) {} diff --git a/pkg/pool/empty_test.go b/pkg/pool/empty_test.go new file mode 100644 index 0000000..235a4a1 --- /dev/null +++ b/pkg/pool/empty_test.go @@ -0,0 +1,25 @@ +// Copyright (c) 2024 The Jaeger Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pool + +import ( + "testing" + + "github.com/jaegertracing/jaeger/pkg/testutils" +) + +func TestMain(m *testing.M) { + testutils.VerifyGoLeaks(m) +} diff --git a/pkg/pool/pool.go b/pkg/pool/pool.go new file mode 100644 index 0000000..4b709d0 --- /dev/null +++ b/pkg/pool/pool.go @@ -0,0 +1,56 @@ +// Copyright (c) 2019 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pool + +// Pool is a simple worker pool +type Pool struct { + jobs chan func() + stop chan struct{} +} + +// New creates a new pool with the given number of workers +func New(workers int) *Pool { + jobs := make(chan func()) + stop := make(chan struct{}) + for i := 0; i < workers; i++ { + go func() { + for { + select { + case job := <-jobs: + job() + case <-stop: + return + } + } + }() + } + return &Pool{ + jobs: jobs, + stop: stop, + } +} + +// Execute enqueues the job to be executed by one of the workers in the pool +func (p *Pool) Execute(job func()) { + p.jobs <- job +} + +// Stop halts all the workers +func (p *Pool) Stop() { + if p.stop != nil { + close(p.stop) + } +} diff --git a/pkg/tracing/baggage.go b/pkg/tracing/baggage.go new file mode 100644 index 0000000..416a33f --- /dev/null +++ b/pkg/tracing/baggage.go @@ -0,0 +1,27 @@ +// Copyright (c) 2023 The Jaeger Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tracing + +import ( + "context" + + "go.opentelemetry.io/otel/baggage" +) + +func BaggageItem(ctx context.Context, key string) string { + b := baggage.FromContext(ctx) + m := b.Member(key) + return m.Value() +} diff --git a/pkg/tracing/empty_test.go b/pkg/tracing/empty_test.go new file mode 100644 index 0000000..04d608c --- /dev/null +++ b/pkg/tracing/empty_test.go @@ -0,0 +1,25 @@ +// Copyright (c) 2024 The Jaeger Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tracing + +import ( + "testing" + + "github.com/jaegertracing/jaeger/pkg/testutils" +) + +func TestMain(m *testing.M) { + testutils.VerifyGoLeaks(m) +} diff --git a/pkg/tracing/http.go b/pkg/tracing/http.go new file mode 100644 index 0000000..dbea5c9 --- /dev/null +++ b/pkg/tracing/http.go @@ -0,0 +1,72 @@ +// Copyright (c) 2019 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tracing + +import ( + "context" + "encoding/json" + "errors" + "io" + "net/http" + + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.opentelemetry.io/otel/trace" +) + +// HTTPClient wraps an http.Client with tracing instrumentation. +type HTTPClient struct { + TracerProvider trace.TracerProvider + Client *http.Client +} + +func NewHTTPClient(tp trace.TracerProvider) *HTTPClient { + return &HTTPClient{ + TracerProvider: tp, + Client: &http.Client{ + Transport: otelhttp.NewTransport( + http.DefaultTransport, + otelhttp.WithTracerProvider(tp), + ), + }, + } +} + +// GetJSON executes HTTP GET against specified url and tried to parse +// the response into out object. +func (c *HTTPClient) GetJSON(ctx context.Context, endpoint string, url string, out interface{}) error { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return err + } + + res, err := c.Client.Do(req) + if err != nil { + return err + } + + defer res.Body.Close() + + if res.StatusCode >= 400 { + body, err := io.ReadAll(res.Body) + if err != nil { + return err + } + return errors.New(string(body)) + } + + decoder := json.NewDecoder(res.Body) + return decoder.Decode(out) +} diff --git a/pkg/tracing/init.go b/pkg/tracing/init.go new file mode 100644 index 0000000..bf2ff03 --- /dev/null +++ b/pkg/tracing/init.go @@ -0,0 +1,111 @@ +// Copyright (c) 2019 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tracing + +import ( + "context" + "errors" + "fmt" + "os" + "strings" + "sync" + "time" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.24.0" + "go.opentelemetry.io/otel/trace" + "go.uber.org/zap" + + "github.com/jaegertracing/jaeger/examples/hotrod/pkg/log" + "github.com/jaegertracing/jaeger/examples/hotrod/pkg/tracing/rpcmetrics" + "github.com/jaegertracing/jaeger/pkg/metrics" +) + +var once sync.Once + +// InitOTEL initializes OpenTelemetry SDK. +func InitOTEL(serviceName string, exporterType string, metricsFactory metrics.Factory, logger log.Factory) trace.TracerProvider { + once.Do(func() { + otel.SetTextMapPropagator( + propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + )) + }) + + exp, err := createOtelExporter(exporterType) + if err != nil { + logger.Bg().Fatal("cannot create exporter", zap.String("exporterType", exporterType), zap.Error(err)) + } + logger.Bg().Debug("using " + exporterType + " trace exporter") + + rpcmetricsObserver := rpcmetrics.NewObserver(metricsFactory, rpcmetrics.DefaultNameNormalizer) + + res, err := resource.New( + context.Background(), + resource.WithSchemaURL(semconv.SchemaURL), + resource.WithAttributes(semconv.ServiceNameKey.String(serviceName)), + resource.WithTelemetrySDK(), + resource.WithHost(), + resource.WithOSType(), + ) + if err != nil { + logger.Bg().Fatal("resource creation failed", zap.Error(err)) + } + + tp := sdktrace.NewTracerProvider( + sdktrace.WithBatcher(exp, sdktrace.WithBatchTimeout(1000*time.Millisecond)), + sdktrace.WithSpanProcessor(rpcmetricsObserver), + sdktrace.WithResource(res), + ) + logger.Bg().Debug("Created OTEL tracer", zap.String("service-name", serviceName)) + return tp +} + +// withSecure instructs the client to use HTTPS scheme, instead of hotrod's desired default HTTP +func withSecure() bool { + return strings.HasPrefix(os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT"), "https://") || + strings.ToLower(os.Getenv("OTEL_EXPORTER_OTLP_INSECURE")) == "false" +} + +func createOtelExporter(exporterType string) (sdktrace.SpanExporter, error) { + var exporter sdktrace.SpanExporter + var err error + switch exporterType { + case "jaeger": + return nil, errors.New("jaeger exporter is no longer supported, please use otlp") + case "otlp": + var opts []otlptracehttp.Option + if !withSecure() { + opts = []otlptracehttp.Option{otlptracehttp.WithInsecure()} + } + exporter, err = otlptrace.New( + context.Background(), + otlptracehttp.NewClient(opts...), + ) + case "stdout": + exporter, err = stdouttrace.New() + default: + return nil, fmt.Errorf("unrecognized exporter type %s", exporterType) + } + return exporter, err +} diff --git a/pkg/tracing/mutex.go b/pkg/tracing/mutex.go new file mode 100644 index 0000000..8e3f7ff --- /dev/null +++ b/pkg/tracing/mutex.go @@ -0,0 +1,86 @@ +// Copyright (c) 2019 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tracing + +import ( + "context" + "fmt" + "sync" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + "go.uber.org/zap" + + "github.com/jaegertracing/jaeger/examples/hotrod/pkg/log" +) + +// Mutex is just like the standard sync.Mutex, except that it is aware of the Context +// and logs some diagnostic information into the current span. +type Mutex struct { + SessionBaggageKey string + LogFactory log.Factory + + realLock sync.Mutex + holder string + + waiters []string + waitersLock sync.Mutex +} + +// Lock acquires an exclusive lock. +func (sm *Mutex) Lock(ctx context.Context) { + logger := sm.LogFactory.For(ctx) + session := BaggageItem(ctx, sm.SessionBaggageKey) + activeSpan := trace.SpanFromContext(ctx) + activeSpan.SetAttributes(attribute.String(sm.SessionBaggageKey, session)) + + sm.waitersLock.Lock() + if waiting := len(sm.waiters); waiting > 0 && activeSpan != nil { + logger.Info( + fmt.Sprintf("Waiting for lock behind %d transactions", waiting), + zap.String("blockers", fmt.Sprintf("%v", sm.waiters)), + ) + } + sm.waiters = append(sm.waiters, session) + sm.waitersLock.Unlock() + + sm.realLock.Lock() + sm.holder = session + + sm.waitersLock.Lock() + behindLen := len(sm.waiters) - 1 + behindIDs := fmt.Sprintf("%v", sm.waiters[1:]) // skip self + sm.waitersLock.Unlock() + + logger.Info( + fmt.Sprintf("Acquired lock; %d transactions waiting behind", behindLen), + zap.String("waiters", behindIDs), + ) +} + +// Unlock releases the lock. +func (sm *Mutex) Unlock() { + sm.waitersLock.Lock() + for i, v := range sm.waiters { + if v == sm.holder { + sm.waiters = append(sm.waiters[0:i], sm.waiters[i+1:]...) + break + } + } + sm.waitersLock.Unlock() + + sm.realLock.Unlock() +} diff --git a/pkg/tracing/mux.go b/pkg/tracing/mux.go new file mode 100644 index 0000000..d003f8f --- /dev/null +++ b/pkg/tracing/mux.go @@ -0,0 +1,77 @@ +// Copyright (c) 2019 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tracing + +import ( + "net/http" + + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/trace" + "go.uber.org/zap" + + "github.com/jaegertracing/jaeger/examples/hotrod/pkg/log" +) + +// NewServeMux creates a new TracedServeMux. +func NewServeMux(copyBaggage bool, tracer trace.TracerProvider, logger log.Factory) *TracedServeMux { + return &TracedServeMux{ + mux: http.NewServeMux(), + copyBaggage: copyBaggage, + tracer: tracer, + logger: logger, + } +} + +// TracedServeMux is a wrapper around http.ServeMux that instruments handlers for tracing. +type TracedServeMux struct { + mux *http.ServeMux + copyBaggage bool + tracer trace.TracerProvider + logger log.Factory +} + +// Handle implements http.ServeMux#Handle, which is used to register new handler. +func (tm *TracedServeMux) Handle(pattern string, handler http.Handler) { + tm.logger.Bg().Debug("registering traced handler", zap.String("endpoint", pattern)) + + middleware := otelhttp.NewHandler( + otelhttp.WithRouteTag(pattern, traceResponseHandler(handler)), + pattern, + otelhttp.WithTracerProvider(tm.tracer)) + + tm.mux.Handle(pattern, middleware) +} + +// ServeHTTP implements http.ServeMux#ServeHTTP. +func (tm *TracedServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { + tm.mux.ServeHTTP(w, r) +} + +// Returns a handler that generates a traceresponse header. +// https://github.com/w3c/trace-context/blob/main/spec/21-http_response_header_format.md +func traceResponseHandler(handler http.Handler) http.Handler { + // We use the standard TraceContext propagator, since the formats are identical. + // But the propagator uses "traceparent" header name, so we inject it into a map + // `carrier` and then use the result to set the "tracereponse" header. + var prop propagation.TraceContext + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + carrier := make(map[string]string) + prop.Inject(r.Context(), propagation.MapCarrier(carrier)) + w.Header().Add("traceresponse", carrier["traceparent"]) + handler.ServeHTTP(w, r) + }) +} diff --git a/pkg/tracing/rpcmetrics/README.md b/pkg/tracing/rpcmetrics/README.md new file mode 100644 index 0000000..34126b6 --- /dev/null +++ b/pkg/tracing/rpcmetrics/README.md @@ -0,0 +1,3 @@ +Package rpcmetrics implements an OpenTelemetry SpanProcessor that can be used to emit RPC metrics. + +This package is copied from jaeger-client-go and adapted to work with OpenTelemtery SDK. diff --git a/pkg/tracing/rpcmetrics/endpoints.go b/pkg/tracing/rpcmetrics/endpoints.go new file mode 100644 index 0000000..7ca73bf --- /dev/null +++ b/pkg/tracing/rpcmetrics/endpoints.go @@ -0,0 +1,63 @@ +// Copyright (c) 2023 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rpcmetrics + +import "sync" + +// normalizedEndpoints is a cache for endpointName -> safeName mappings. +type normalizedEndpoints struct { + names map[string]string + maxSize int + normalizer NameNormalizer + mux sync.RWMutex +} + +func newNormalizedEndpoints(maxSize int, normalizer NameNormalizer) *normalizedEndpoints { + return &normalizedEndpoints{ + maxSize: maxSize, + normalizer: normalizer, + names: make(map[string]string, maxSize), + } +} + +// normalize looks up the name in the cache, if not found it uses normalizer +// to convert the name to a safe name. If called with more than maxSize unique +// names it returns "" for all other names beyond those already cached. +func (n *normalizedEndpoints) normalize(name string) string { + n.mux.RLock() + norm, ok := n.names[name] + l := len(n.names) + n.mux.RUnlock() + if ok { + return norm + } + if l >= n.maxSize { + return "" + } + return n.normalizeWithLock(name) +} + +func (n *normalizedEndpoints) normalizeWithLock(name string) string { + norm := n.normalizer.Normalize(name) + n.mux.Lock() + defer n.mux.Unlock() + // cache may have grown while we were not holding the lock + if len(n.names) >= n.maxSize { + return "" + } + n.names[name] = norm + return norm +} diff --git a/pkg/tracing/rpcmetrics/endpoints_test.go b/pkg/tracing/rpcmetrics/endpoints_test.go new file mode 100644 index 0000000..fc1f84f --- /dev/null +++ b/pkg/tracing/rpcmetrics/endpoints_test.go @@ -0,0 +1,44 @@ +// Copyright (c) 2023 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rpcmetrics + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNormalizedEndpoints(t *testing.T) { + n := newNormalizedEndpoints(1, DefaultNameNormalizer) + + assertLen := func(l int) { + n.mux.RLock() + defer n.mux.RUnlock() + assert.Len(t, n.names, l) + } + + assert.Equal(t, "ab_cd", n.normalize("ab^cd"), "one translation") + assert.Equal(t, "ab_cd", n.normalize("ab^cd"), "cache hit") + assertLen(1) + assert.Equal(t, "", n.normalize("xys"), "cache overflow") + assertLen(1) +} + +func TestNormalizedEndpointsDoubleLocking(t *testing.T) { + n := newNormalizedEndpoints(1, DefaultNameNormalizer) + assert.Equal(t, "ab_cd", n.normalize("ab^cd"), "fill out the cache") + assert.Equal(t, "", n.normalizeWithLock("xys"), "cache overflow") +} diff --git a/pkg/tracing/rpcmetrics/metrics.go b/pkg/tracing/rpcmetrics/metrics.go new file mode 100644 index 0000000..561615f --- /dev/null +++ b/pkg/tracing/rpcmetrics/metrics.go @@ -0,0 +1,126 @@ +// Copyright (c) 2023 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rpcmetrics + +import ( + "sync" + + "github.com/jaegertracing/jaeger/pkg/metrics" +) + +const ( + otherEndpointsPlaceholder = "other" + endpointNameMetricTag = "endpoint" +) + +// Metrics is a collection of metrics for an endpoint describing +// throughput, success, errors, and performance. +type Metrics struct { + // RequestCountSuccess is a counter of the total number of successes. + RequestCountSuccess metrics.Counter `metric:"requests" tags:"error=false"` + + // RequestCountFailures is a counter of the number of times any failure has been observed. + RequestCountFailures metrics.Counter `metric:"requests" tags:"error=true"` + + // RequestLatencySuccess is a latency histogram of successful requests. + RequestLatencySuccess metrics.Timer `metric:"request_latency" tags:"error=false"` + + // RequestLatencyFailures is a latency histogram of failed requests. + RequestLatencyFailures metrics.Timer `metric:"request_latency" tags:"error=true"` + + // HTTPStatusCode2xx is a counter of the total number of requests with HTTP status code 200-299 + HTTPStatusCode2xx metrics.Counter `metric:"http_requests" tags:"status_code=2xx"` + + // HTTPStatusCode3xx is a counter of the total number of requests with HTTP status code 300-399 + HTTPStatusCode3xx metrics.Counter `metric:"http_requests" tags:"status_code=3xx"` + + // HTTPStatusCode4xx is a counter of the total number of requests with HTTP status code 400-499 + HTTPStatusCode4xx metrics.Counter `metric:"http_requests" tags:"status_code=4xx"` + + // HTTPStatusCode5xx is a counter of the total number of requests with HTTP status code 500-599 + HTTPStatusCode5xx metrics.Counter `metric:"http_requests" tags:"status_code=5xx"` +} + +func (m *Metrics) recordHTTPStatusCode(statusCode int64) { + switch { + case statusCode >= 200 && statusCode < 300: + m.HTTPStatusCode2xx.Inc(1) + case statusCode >= 300 && statusCode < 400: + m.HTTPStatusCode3xx.Inc(1) + case statusCode >= 400 && statusCode < 500: + m.HTTPStatusCode4xx.Inc(1) + case statusCode >= 500 && statusCode < 600: + m.HTTPStatusCode5xx.Inc(1) + } +} + +// MetricsByEndpoint is a registry/cache of metrics for each unique endpoint name. +// Only maxNumberOfEndpoints Metrics are stored, all other endpoint names are mapped +// to a generic endpoint name "other". +type MetricsByEndpoint struct { + metricsFactory metrics.Factory + endpoints *normalizedEndpoints + metricsByEndpoint map[string]*Metrics + mux sync.RWMutex +} + +func newMetricsByEndpoint( + metricsFactory metrics.Factory, + normalizer NameNormalizer, + maxNumberOfEndpoints int, +) *MetricsByEndpoint { + return &MetricsByEndpoint{ + metricsFactory: metricsFactory, + endpoints: newNormalizedEndpoints(maxNumberOfEndpoints, normalizer), + metricsByEndpoint: make(map[string]*Metrics, maxNumberOfEndpoints+1), // +1 for "other" + } +} + +func (m *MetricsByEndpoint) get(endpoint string) *Metrics { + safeName := m.endpoints.normalize(endpoint) + if safeName == "" { + safeName = otherEndpointsPlaceholder + } + m.mux.RLock() + met := m.metricsByEndpoint[safeName] + m.mux.RUnlock() + if met != nil { + return met + } + + return m.getWithWriteLock(safeName) +} + +// split to make easier to test +func (m *MetricsByEndpoint) getWithWriteLock(safeName string) *Metrics { + m.mux.Lock() + defer m.mux.Unlock() + + // it is possible that the name has been already registered after we released + // the read lock and before we grabbed the write lock, so check for that. + if met, ok := m.metricsByEndpoint[safeName]; ok { + return met + } + + // it would be nice to create the struct before locking, since Init() is somewhat + // expensive, however some metrics backends (e.g. expvar) may not like duplicate metrics. + met := &Metrics{} + tags := map[string]string{endpointNameMetricTag: safeName} + metrics.Init(met, m.metricsFactory, tags) + + m.metricsByEndpoint[safeName] = met + return met +} diff --git a/pkg/tracing/rpcmetrics/metrics_test.go b/pkg/tracing/rpcmetrics/metrics_test.go new file mode 100644 index 0000000..3db6181 --- /dev/null +++ b/pkg/tracing/rpcmetrics/metrics_test.go @@ -0,0 +1,62 @@ +// Copyright (c) 2023 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rpcmetrics + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/jaegertracing/jaeger/internal/metricstest" +) + +// E.g. tags("key", "value", "key", "value") +func tags(kv ...string) map[string]string { + m := make(map[string]string) + for i := 0; i < len(kv)-1; i += 2 { + m[kv[i]] = kv[i+1] + } + return m +} + +func endpointTags(endpoint string, kv ...string) map[string]string { + return tags(append([]string{"endpoint", endpoint}, kv...)...) +} + +func TestMetricsByEndpoint(t *testing.T) { + met := metricstest.NewFactory(0) + mbe := newMetricsByEndpoint(met, DefaultNameNormalizer, 2) + + m1 := mbe.get("abc1") + m2 := mbe.get("abc1") // from cache + m2a := mbe.getWithWriteLock("abc1") // from cache in double-checked lock + assert.Equal(t, m1, m2) + assert.Equal(t, m1, m2a) + + m3 := mbe.get("abc3") + m4 := mbe.get("overflow") + m5 := mbe.get("overflow2") + + for _, m := range []*Metrics{m1, m2, m2a, m3, m4, m5} { + m.RequestCountSuccess.Inc(1) + } + + met.AssertCounterMetrics(t, + metricstest.ExpectedMetric{Name: "requests", Tags: endpointTags("abc1", "error", "false"), Value: 3}, + metricstest.ExpectedMetric{Name: "requests", Tags: endpointTags("abc3", "error", "false"), Value: 1}, + metricstest.ExpectedMetric{Name: "requests", Tags: endpointTags("other", "error", "false"), Value: 2}, + ) +} diff --git a/pkg/tracing/rpcmetrics/normalizer.go b/pkg/tracing/rpcmetrics/normalizer.go new file mode 100644 index 0000000..01f5bcd --- /dev/null +++ b/pkg/tracing/rpcmetrics/normalizer.go @@ -0,0 +1,101 @@ +// Copyright (c) 2023 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rpcmetrics + +// NameNormalizer is used to convert the endpoint names to strings +// that can be safely used as tags in the metrics. +type NameNormalizer interface { + Normalize(name string) string +} + +// DefaultNameNormalizer converts endpoint names so that they contain only characters +// from the safe charset [a-zA-Z0-9./_]. All other characters are replaced with '_'. +var DefaultNameNormalizer = &SimpleNameNormalizer{ + SafeSets: []SafeCharacterSet{ + &Range{From: 'a', To: 'z'}, + &Range{From: 'A', To: 'Z'}, + &Range{From: '0', To: '9'}, + &Char{'_'}, + &Char{'/'}, + &Char{'.'}, + }, + Replacement: '_', +} + +// SimpleNameNormalizer uses a set of safe character sets. +type SimpleNameNormalizer struct { + SafeSets []SafeCharacterSet + Replacement byte +} + +// SafeCharacterSet determines if the given character is "safe" +type SafeCharacterSet interface { + IsSafe(c byte) bool +} + +// Range implements SafeCharacterSet +type Range struct { + From, To byte +} + +// IsSafe implements SafeCharacterSet +func (r *Range) IsSafe(c byte) bool { + return c >= r.From && c <= r.To +} + +// Char implements SafeCharacterSet +type Char struct { + Val byte +} + +// IsSafe implements SafeCharacterSet +func (ch *Char) IsSafe(c byte) bool { + return c == ch.Val +} + +// Normalize checks each character in the string against SafeSets, +// and if it's not safe substitutes it with Replacement. +func (n *SimpleNameNormalizer) Normalize(name string) string { + var retMe []byte + nameBytes := []byte(name) + for i, b := range nameBytes { + if n.safeByte(b) { + if retMe != nil { + retMe[i] = b + } + } else { + if retMe == nil { + retMe = make([]byte, len(nameBytes)) + copy(retMe[0:i], nameBytes[0:i]) + } + retMe[i] = n.Replacement + } + } + if retMe == nil { + return name + } + return string(retMe) +} + +// safeByte checks if b against all safe charsets. +func (n *SimpleNameNormalizer) safeByte(b byte) bool { + for i := range n.SafeSets { + if n.SafeSets[i].IsSafe(b) { + return true + } + } + return false +} diff --git a/pkg/tracing/rpcmetrics/normalizer_test.go b/pkg/tracing/rpcmetrics/normalizer_test.go new file mode 100644 index 0000000..dac1ff7 --- /dev/null +++ b/pkg/tracing/rpcmetrics/normalizer_test.go @@ -0,0 +1,35 @@ +// Copyright (c) 2023 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rpcmetrics + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSimpleNameNormalizer(t *testing.T) { + n := &SimpleNameNormalizer{ + SafeSets: []SafeCharacterSet{ + &Range{From: 'a', To: 'z'}, + &Char{'-'}, + }, + Replacement: '-', + } + assert.Equal(t, "ab-cd", n.Normalize("ab-cd"), "all valid") + assert.Equal(t, "ab-cd", n.Normalize("ab.cd"), "single mismatch") + assert.Equal(t, "a--cd", n.Normalize("aB-cd"), "range letter mismatch") +} diff --git a/pkg/tracing/rpcmetrics/observer.go b/pkg/tracing/rpcmetrics/observer.go new file mode 100644 index 0000000..ff03613 --- /dev/null +++ b/pkg/tracing/rpcmetrics/observer.go @@ -0,0 +1,92 @@ +// Copyright (c) 2023 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rpcmetrics + +import ( + "context" + "strconv" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.24.0" + "go.opentelemetry.io/otel/trace" + + "github.com/jaegertracing/jaeger/pkg/metrics" +) + +const defaultMaxNumberOfEndpoints = 200 + +var _ sdktrace.SpanProcessor = (*Observer)(nil) + +// Observer is an observer that can emit RPC metrics. +type Observer struct { + metricsByEndpoint *MetricsByEndpoint +} + +// NewObserver creates a new observer that can emit RPC metrics. +func NewObserver(metricsFactory metrics.Factory, normalizer NameNormalizer) *Observer { + return &Observer{ + metricsByEndpoint: newMetricsByEndpoint( + metricsFactory, + normalizer, + defaultMaxNumberOfEndpoints, + ), + } +} + +func (o *Observer) OnStart(parent context.Context, s sdktrace.ReadWriteSpan) {} + +func (o *Observer) OnEnd(sp sdktrace.ReadOnlySpan) { + operationName := sp.Name() + if operationName == "" { + return + } + if sp.SpanKind() != trace.SpanKindServer { + return + } + + mets := o.metricsByEndpoint.get(operationName) + latency := sp.EndTime().Sub(sp.StartTime()) + + if status := sp.Status(); status.Code == codes.Error { + mets.RequestCountFailures.Inc(1) + mets.RequestLatencyFailures.Record(latency) + } else { + mets.RequestCountSuccess.Inc(1) + mets.RequestLatencySuccess.Record(latency) + } + for _, attr := range sp.Attributes() { + if string(attr.Key) == string(semconv.HTTPResponseStatusCodeKey) { + if attr.Value.Type() == attribute.INT64 { + mets.recordHTTPStatusCode(attr.Value.AsInt64()) + } else if attr.Value.Type() == attribute.STRING { + s := attr.Value.AsString() + if n, err := strconv.Atoi(s); err == nil { + mets.recordHTTPStatusCode(int64(n)) + } + } + } + } +} + +func (o *Observer) Shutdown(ctx context.Context) error { + return nil +} + +func (o *Observer) ForceFlush(ctx context.Context) error { + return nil +} diff --git a/pkg/tracing/rpcmetrics/observer_test.go b/pkg/tracing/rpcmetrics/observer_test.go new file mode 100644 index 0000000..6e92180 --- /dev/null +++ b/pkg/tracing/rpcmetrics/observer_test.go @@ -0,0 +1,149 @@ +// Copyright (c) 2023 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rpcmetrics + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.24.0" + "go.opentelemetry.io/otel/trace" + + u "github.com/jaegertracing/jaeger/internal/metricstest" +) + +type testTracer struct { + metrics *u.Factory + tracer trace.Tracer +} + +func withTestTracer(runTest func(tt *testTracer)) { + metrics := u.NewFactory(time.Minute) + defer metrics.Stop() + observer := NewObserver(metrics, DefaultNameNormalizer) + + tp := sdktrace.NewTracerProvider( + sdktrace.WithSpanProcessor(observer), + sdktrace.WithResource(resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceNameKey.String("test"), + )), + ) + runTest(&testTracer{ + metrics: metrics, + tracer: tp.Tracer("test"), + }) +} + +func TestObserver(t *testing.T) { + withTestTracer(func(testTracer *testTracer) { + ts := time.Now() + finishOptions := trace.WithTimestamp(ts.Add((50 * time.Millisecond))) + + testCases := []struct { + name string + spanKind trace.SpanKind + opNameOverride string + err bool + }{ + {name: "local-span", spanKind: trace.SpanKindInternal}, + {name: "get-user", spanKind: trace.SpanKindServer}, + {name: "get-user", spanKind: trace.SpanKindServer, opNameOverride: "get-user-override"}, + {name: "get-user", spanKind: trace.SpanKindServer, err: true}, + {name: "get-user-client", spanKind: trace.SpanKindClient}, + } + + for _, testCase := range testCases { + _, span := testTracer.tracer.Start( + context.Background(), + testCase.name, trace.WithSpanKind(testCase.spanKind), + trace.WithTimestamp(ts), + ) + if testCase.opNameOverride != "" { + span.SetName(testCase.opNameOverride) + } + if testCase.err { + span.SetStatus(codes.Error, "An error occurred") + } + span.End(finishOptions) + } + + testTracer.metrics.AssertCounterMetrics(t, + u.ExpectedMetric{Name: "requests", Tags: endpointTags("local_span", "error", "false"), Value: 0}, + u.ExpectedMetric{Name: "requests", Tags: endpointTags("get_user", "error", "false"), Value: 1}, + u.ExpectedMetric{Name: "requests", Tags: endpointTags("get_user", "error", "true"), Value: 1}, + u.ExpectedMetric{Name: "requests", Tags: endpointTags("get_user_override", "error", "false"), Value: 1}, + u.ExpectedMetric{Name: "requests", Tags: endpointTags("get_user_client", "error", "false"), Value: 0}, + ) + // TODO something wrong with string generation, .P99 should not be appended to the tag + // as a result we cannot use u.AssertGaugeMetrics + _, g := testTracer.metrics.Snapshot() + assert.EqualValues(t, 51, g["request_latency|endpoint=get_user|error=false.P99"]) + assert.EqualValues(t, 51, g["request_latency|endpoint=get_user|error=true.P99"]) + }) +} + +func TestTags(t *testing.T) { + type tagTestCase struct { + attr attribute.KeyValue + err bool + metrics []u.ExpectedMetric + } + testCases := []tagTestCase{ + {err: false, metrics: []u.ExpectedMetric{ + {Name: "requests", Value: 1, Tags: tags("error", "false")}, + }}, + {err: true, metrics: []u.ExpectedMetric{ + {Name: "requests", Value: 1, Tags: tags("error", "true")}, + }}, + } + + for i := 200; i <= 500; i += 100 { + testCases = append(testCases, tagTestCase{ + attr: semconv.HTTPResponseStatusCode(i), + metrics: []u.ExpectedMetric{ + {Name: "http_requests", Value: 1, Tags: tags("status_code", fmt.Sprintf("%dxx", i/100))}, + }, + }) + } + + for _, testCase := range testCases { + for i := range testCase.metrics { + testCase.metrics[i].Tags["endpoint"] = "span" + } + t.Run(fmt.Sprintf("%s-%v", testCase.attr.Key, testCase.attr.Value), func(t *testing.T) { + withTestTracer(func(testTracer *testTracer) { + _, span := testTracer.tracer.Start( + context.Background(), + "span", trace.WithSpanKind(trace.SpanKindServer), + ) + span.SetAttributes(testCase.attr) + if testCase.err { + span.SetStatus(codes.Error, "An error occurred") + } + span.End() + testTracer.metrics.AssertCounterMetrics(t, testCase.metrics...) + }) + }) + } +} diff --git a/pkg/tracing/rpcmetrics/package_test.go b/pkg/tracing/rpcmetrics/package_test.go new file mode 100644 index 0000000..81f8e8e --- /dev/null +++ b/pkg/tracing/rpcmetrics/package_test.go @@ -0,0 +1,25 @@ +// Copyright (c) 2023 The Jaeger Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rpcmetrics + +import ( + "testing" + + "github.com/jaegertracing/jaeger/pkg/testutils" +) + +func TestMain(m *testing.M) { + testutils.VerifyGoLeaks(m) +} diff --git a/services/config/config.go b/services/config/config.go new file mode 100644 index 0000000..bf3ff56 --- /dev/null +++ b/services/config/config.go @@ -0,0 +1,63 @@ +// Copyright (c) 2019 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +import ( + "time" +) + +var ( + // 'frontend' service + + // RouteWorkerPoolSize is the size of the worker pool used to query `route` service. + // Can be overwritten from command line. + RouteWorkerPoolSize = 3 + + // 'customer' service + + // MySQLGetDelay is how long retrieving a customer record takes. + // Using large value mostly because I cannot click the button fast enough to cause a queue. + MySQLGetDelay = 300 * time.Millisecond + + // MySQLGetDelayStdDev is standard deviation + MySQLGetDelayStdDev = MySQLGetDelay / 10 + + // MySQLMutexDisabled controls whether there is a mutex guarding db query execution. + // When not disabled it simulates a misconfigured connection pool of size 1. + MySQLMutexDisabled = false + + // 'driver' service + + // RedisFindDelay is how long finding closest drivers takes. + RedisFindDelay = 20 * time.Millisecond + + // RedisFindDelayStdDev is standard deviation. + RedisFindDelayStdDev = RedisFindDelay / 4 + + // RedisGetDelay is how long retrieving a driver record takes. + RedisGetDelay = 10 * time.Millisecond + + // RedisGetDelayStdDev is standard deviation + RedisGetDelayStdDev = RedisGetDelay / 4 + + // 'route' service + + // RouteCalcDelay is how long a route calculation takes + RouteCalcDelay = 50 * time.Millisecond + + // RouteCalcDelayStdDev is standard deviation + RouteCalcDelayStdDev = RouteCalcDelay / 4 +) diff --git a/services/config/empty_test.go b/services/config/empty_test.go new file mode 100644 index 0000000..943a572 --- /dev/null +++ b/services/config/empty_test.go @@ -0,0 +1,25 @@ +// Copyright (c) 2024 The Jaeger Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +import ( + "testing" + + "github.com/jaegertracing/jaeger/pkg/testutils" +) + +func TestMain(m *testing.M) { + testutils.VerifyGoLeaks(m) +} diff --git a/services/customer/client.go b/services/customer/client.go new file mode 100644 index 0000000..56f0cb9 --- /dev/null +++ b/services/customer/client.go @@ -0,0 +1,55 @@ +// Copyright (c) 2019 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package customer + +import ( + "context" + "fmt" + + "go.opentelemetry.io/otel/trace" + "go.uber.org/zap" + + "github.com/jaegertracing/jaeger/examples/hotrod/pkg/log" + "github.com/jaegertracing/jaeger/examples/hotrod/pkg/tracing" +) + +// Client is a remote client that implements customer.Interface +type Client struct { + logger log.Factory + client *tracing.HTTPClient + hostPort string +} + +// NewClient creates a new customer.Client +func NewClient(tracer trace.TracerProvider, logger log.Factory, hostPort string) *Client { + return &Client{ + logger: logger, + client: tracing.NewHTTPClient(tracer), + hostPort: hostPort, + } +} + +// Get implements customer.Interface#Get as an RPC +func (c *Client) Get(ctx context.Context, customerID int) (*Customer, error) { + c.logger.For(ctx).Info("Getting customer", zap.Int("customer_id", customerID)) + + url := fmt.Sprintf("http://"+c.hostPort+"/customer?customer=%d", customerID) + var customer Customer + if err := c.client.GetJSON(ctx, "/customer", url, &customer); err != nil { + return nil, err + } + return &customer, nil +} diff --git a/services/customer/database.go b/services/customer/database.go new file mode 100644 index 0000000..ce72eff --- /dev/null +++ b/services/customer/database.go @@ -0,0 +1,100 @@ +// Copyright (c) 2019 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package customer + +import ( + "context" + "errors" + "fmt" + + "go.opentelemetry.io/otel/attribute" + semconv "go.opentelemetry.io/otel/semconv/v1.24.0" + "go.opentelemetry.io/otel/trace" + "go.uber.org/zap" + + "github.com/jaegertracing/jaeger/examples/hotrod/pkg/delay" + "github.com/jaegertracing/jaeger/examples/hotrod/pkg/log" + "github.com/jaegertracing/jaeger/examples/hotrod/pkg/tracing" + "github.com/jaegertracing/jaeger/examples/hotrod/services/config" +) + +// database simulates Customer repository implemented on top of an SQL database +type database struct { + tracer trace.Tracer + logger log.Factory + customers map[int]*Customer + lock *tracing.Mutex +} + +func newDatabase(tracer trace.Tracer, logger log.Factory) *database { + return &database{ + tracer: tracer, + logger: logger, + lock: &tracing.Mutex{ + SessionBaggageKey: "request", + LogFactory: logger, + }, + customers: map[int]*Customer{ + 123: { + ID: "123", + Name: "Rachel's_Floral_Designs", + Location: "115,277", + }, + 567: { + ID: "567", + Name: "Amazing_Coffee_Roasters", + Location: "211,653", + }, + 392: { + ID: "392", + Name: "Trom_Chocolatier", + Location: "577,322", + }, + 731: { + ID: "731", + Name: "Japanese_Desserts", + Location: "728,326", + }, + }, + } +} + +func (d *database) Get(ctx context.Context, customerID int) (*Customer, error) { + d.logger.For(ctx).Info("Loading customer", zap.Int("customer_id", customerID)) + + ctx, span := d.tracer.Start(ctx, "SQL SELECT", trace.WithSpanKind(trace.SpanKindClient)) + span.SetAttributes( + semconv.PeerServiceKey.String("mysql"), + attribute. + Key("sql.query"). + String(fmt.Sprintf("SELECT * FROM customer WHERE customer_id=%d", customerID)), + ) + defer span.End() + + if !config.MySQLMutexDisabled { + // simulate misconfigured connection pool that only gives one connection at a time + d.lock.Lock(ctx) + defer d.lock.Unlock() + } + + // simulate RPC delay + delay.Sleep(config.MySQLGetDelay, config.MySQLGetDelayStdDev) + + if customer, ok := d.customers[customerID]; ok { + return customer, nil + } + return nil, errors.New("invalid customer ID") +} diff --git a/services/customer/empty_test.go b/services/customer/empty_test.go new file mode 100644 index 0000000..d9c068b --- /dev/null +++ b/services/customer/empty_test.go @@ -0,0 +1,25 @@ +// Copyright (c) 2024 The Jaeger Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package customer + +import ( + "testing" + + "github.com/jaegertracing/jaeger/pkg/testutils" +) + +func TestMain(m *testing.M) { + testutils.VerifyGoLeaks(m) +} diff --git a/services/customer/interface.go b/services/customer/interface.go new file mode 100644 index 0000000..e144832 --- /dev/null +++ b/services/customer/interface.go @@ -0,0 +1,32 @@ +// Copyright (c) 2019 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package customer + +import ( + "context" +) + +// Customer contains data about a customer. +type Customer struct { + ID string + Name string + Location string +} + +// Interface exposed by the Customer service. +type Interface interface { + Get(ctx context.Context, customerID int) (*Customer, error) +} diff --git a/services/customer/server.go b/services/customer/server.go new file mode 100644 index 0000000..e353c20 --- /dev/null +++ b/services/customer/server.go @@ -0,0 +1,105 @@ +// Copyright (c) 2019 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package customer + +import ( + "encoding/json" + "net/http" + "strconv" + "time" + + "go.opentelemetry.io/otel/trace" + "go.uber.org/zap" + + "github.com/jaegertracing/jaeger/examples/hotrod/pkg/httperr" + "github.com/jaegertracing/jaeger/examples/hotrod/pkg/log" + "github.com/jaegertracing/jaeger/examples/hotrod/pkg/tracing" + "github.com/jaegertracing/jaeger/pkg/metrics" +) + +// Server implements Customer service +type Server struct { + hostPort string + tracer trace.TracerProvider + logger log.Factory + database *database +} + +// NewServer creates a new customer.Server +func NewServer(hostPort string, otelExporter string, metricsFactory metrics.Factory, logger log.Factory) *Server { + return &Server{ + hostPort: hostPort, + tracer: tracing.InitOTEL("customer", otelExporter, metricsFactory, logger), + logger: logger, + database: newDatabase( + tracing.InitOTEL("mysql", otelExporter, metricsFactory, logger).Tracer("mysql"), + logger.With(zap.String("component", "mysql")), + ), + } +} + +// Run starts the Customer server +func (s *Server) Run() error { + mux := s.createServeMux() + s.logger.Bg().Info("Starting", zap.String("address", "http://"+s.hostPort)) + server := &http.Server{ + Addr: s.hostPort, + Handler: mux, + ReadHeaderTimeout: 3 * time.Second, + } + return server.ListenAndServe() +} + +func (s *Server) createServeMux() http.Handler { + mux := tracing.NewServeMux(false, s.tracer, s.logger) + mux.Handle("/customer", http.HandlerFunc(s.customer)) + return mux +} + +func (s *Server) customer(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + s.logger.For(ctx).Info("HTTP request received", zap.String("method", r.Method), zap.Stringer("url", r.URL)) + if err := r.ParseForm(); httperr.HandleError(w, err, http.StatusBadRequest) { + s.logger.For(ctx).Error("bad request", zap.Error(err)) + return + } + + customer := r.Form.Get("customer") + if customer == "" { + http.Error(w, "Missing required 'customer' parameter", http.StatusBadRequest) + return + } + customerID, err := strconv.Atoi(customer) + if err != nil { + http.Error(w, "Parameter 'customer' is not an integer", http.StatusBadRequest) + return + } + + response, err := s.database.Get(ctx, customerID) + if httperr.HandleError(w, err, http.StatusInternalServerError) { + s.logger.For(ctx).Error("request failed", zap.Error(err)) + return + } + + data, err := json.Marshal(response) + if httperr.HandleError(w, err, http.StatusInternalServerError) { + s.logger.For(ctx).Error("cannot marshal response", zap.Error(err)) + return + } + + w.Header().Set("Content-Type", "application/json") + w.Write(data) +} diff --git a/services/driver/client.go b/services/driver/client.go new file mode 100644 index 0000000..e40c8b8 --- /dev/null +++ b/services/driver/client.go @@ -0,0 +1,75 @@ +// Copyright (c) 2019 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package driver + +import ( + "context" + "time" + + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" + "go.opentelemetry.io/otel/trace" + "go.uber.org/zap" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + "github.com/jaegertracing/jaeger/examples/hotrod/pkg/log" +) + +// Client is a remote client that implements driver.Interface +type Client struct { + logger log.Factory + client DriverServiceClient +} + +// NewClient creates a new driver.Client +func NewClient(tracerProvider trace.TracerProvider, logger log.Factory, hostPort string) *Client { + conn, err := grpc.Dial(hostPort, + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithStatsHandler(otelgrpc.NewClientHandler(otelgrpc.WithTracerProvider(tracerProvider))), + ) + if err != nil { + logger.Bg().Fatal("Cannot create gRPC connection", zap.Error(err)) + } + + client := NewDriverServiceClient(conn) + return &Client{ + logger: logger, + client: client, + } +} + +// FindNearest implements driver.Interface#FindNearest as an RPC +func (c *Client) FindNearest(ctx context.Context, location string) ([]Driver, error) { + c.logger.For(ctx).Info("Finding nearest drivers", zap.String("location", location)) + ctx, cancel := context.WithTimeout(ctx, 1*time.Second) + defer cancel() + response, err := c.client.FindNearest(ctx, &DriverLocationRequest{Location: location}) + if err != nil { + return nil, err + } + return fromProto(response), nil +} + +func fromProto(response *DriverLocationResponse) []Driver { + retMe := make([]Driver, len(response.Locations)) + for i, result := range response.Locations { + retMe[i] = Driver{ + DriverID: result.DriverID, + Location: result.Location, + } + } + return retMe +} diff --git a/services/driver/driver.pb.go b/services/driver/driver.pb.go new file mode 100644 index 0000000..3e4e594 --- /dev/null +++ b/services/driver/driver.pb.go @@ -0,0 +1,254 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: examples/hotrod/services/driver/driver.proto + +package driver + +import ( + context "context" + fmt "fmt" + proto "github.com/gogo/protobuf/proto" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + math "math" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +type DriverLocationRequest struct { + Location string `protobuf:"bytes,1,opt,name=location,proto3" json:"location,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *DriverLocationRequest) Reset() { *m = DriverLocationRequest{} } +func (m *DriverLocationRequest) String() string { return proto.CompactTextString(m) } +func (*DriverLocationRequest) ProtoMessage() {} +func (*DriverLocationRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_cdcd28b7ebdcd54f, []int{0} +} +func (m *DriverLocationRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_DriverLocationRequest.Unmarshal(m, b) +} +func (m *DriverLocationRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_DriverLocationRequest.Marshal(b, m, deterministic) +} +func (m *DriverLocationRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_DriverLocationRequest.Merge(m, src) +} +func (m *DriverLocationRequest) XXX_Size() int { + return xxx_messageInfo_DriverLocationRequest.Size(m) +} +func (m *DriverLocationRequest) XXX_DiscardUnknown() { + xxx_messageInfo_DriverLocationRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_DriverLocationRequest proto.InternalMessageInfo + +func (m *DriverLocationRequest) GetLocation() string { + if m != nil { + return m.Location + } + return "" +} + +type DriverLocation struct { + DriverID string `protobuf:"bytes,1,opt,name=driverID,proto3" json:"driverID,omitempty"` + Location string `protobuf:"bytes,2,opt,name=location,proto3" json:"location,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *DriverLocation) Reset() { *m = DriverLocation{} } +func (m *DriverLocation) String() string { return proto.CompactTextString(m) } +func (*DriverLocation) ProtoMessage() {} +func (*DriverLocation) Descriptor() ([]byte, []int) { + return fileDescriptor_cdcd28b7ebdcd54f, []int{1} +} +func (m *DriverLocation) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_DriverLocation.Unmarshal(m, b) +} +func (m *DriverLocation) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_DriverLocation.Marshal(b, m, deterministic) +} +func (m *DriverLocation) XXX_Merge(src proto.Message) { + xxx_messageInfo_DriverLocation.Merge(m, src) +} +func (m *DriverLocation) XXX_Size() int { + return xxx_messageInfo_DriverLocation.Size(m) +} +func (m *DriverLocation) XXX_DiscardUnknown() { + xxx_messageInfo_DriverLocation.DiscardUnknown(m) +} + +var xxx_messageInfo_DriverLocation proto.InternalMessageInfo + +func (m *DriverLocation) GetDriverID() string { + if m != nil { + return m.DriverID + } + return "" +} + +func (m *DriverLocation) GetLocation() string { + if m != nil { + return m.Location + } + return "" +} + +type DriverLocationResponse struct { + Locations []*DriverLocation `protobuf:"bytes,1,rep,name=locations,proto3" json:"locations,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *DriverLocationResponse) Reset() { *m = DriverLocationResponse{} } +func (m *DriverLocationResponse) String() string { return proto.CompactTextString(m) } +func (*DriverLocationResponse) ProtoMessage() {} +func (*DriverLocationResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_cdcd28b7ebdcd54f, []int{2} +} +func (m *DriverLocationResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_DriverLocationResponse.Unmarshal(m, b) +} +func (m *DriverLocationResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_DriverLocationResponse.Marshal(b, m, deterministic) +} +func (m *DriverLocationResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_DriverLocationResponse.Merge(m, src) +} +func (m *DriverLocationResponse) XXX_Size() int { + return xxx_messageInfo_DriverLocationResponse.Size(m) +} +func (m *DriverLocationResponse) XXX_DiscardUnknown() { + xxx_messageInfo_DriverLocationResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_DriverLocationResponse proto.InternalMessageInfo + +func (m *DriverLocationResponse) GetLocations() []*DriverLocation { + if m != nil { + return m.Locations + } + return nil +} + +func init() { + proto.RegisterType((*DriverLocationRequest)(nil), "driver.DriverLocationRequest") + proto.RegisterType((*DriverLocation)(nil), "driver.DriverLocation") + proto.RegisterType((*DriverLocationResponse)(nil), "driver.DriverLocationResponse") +} + +func init() { + proto.RegisterFile("examples/hotrod/services/driver/driver.proto", fileDescriptor_cdcd28b7ebdcd54f) +} + +var fileDescriptor_cdcd28b7ebdcd54f = []byte{ + // 207 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xd2, 0x49, 0xad, 0x48, 0xcc, + 0x2d, 0xc8, 0x49, 0x2d, 0xd6, 0xcf, 0xc8, 0x2f, 0x29, 0xca, 0x4f, 0xd1, 0x2f, 0x4e, 0x2d, 0x2a, + 0xcb, 0x4c, 0x4e, 0x2d, 0xd6, 0x4f, 0x29, 0xca, 0x2c, 0x4b, 0x2d, 0x82, 0x52, 0x7a, 0x05, 0x45, + 0xf9, 0x25, 0xf9, 0x42, 0x6c, 0x10, 0x9e, 0x92, 0x31, 0x97, 0xa8, 0x0b, 0x98, 0xe5, 0x93, 0x9f, + 0x9c, 0x58, 0x92, 0x99, 0x9f, 0x17, 0x94, 0x5a, 0x58, 0x9a, 0x5a, 0x5c, 0x22, 0x24, 0xc5, 0xc5, + 0x91, 0x03, 0x15, 0x92, 0x60, 0x54, 0x60, 0xd4, 0xe0, 0x0c, 0x82, 0xf3, 0x95, 0x3c, 0xb8, 0xf8, + 0x50, 0x35, 0x81, 0x54, 0x43, 0x0c, 0xf4, 0x74, 0x81, 0xa9, 0x86, 0xf1, 0x51, 0x4c, 0x62, 0x42, + 0x33, 0xc9, 0x8f, 0x4b, 0x0c, 0xdd, 0xfa, 0xe2, 0x82, 0xfc, 0xbc, 0xe2, 0x54, 0x21, 0x13, 0x2e, + 0x4e, 0x98, 0xaa, 0x62, 0x09, 0x46, 0x05, 0x66, 0x0d, 0x6e, 0x23, 0x31, 0x3d, 0xa8, 0x17, 0xd0, + 0xb4, 0x20, 0x14, 0x1a, 0xc5, 0x72, 0xf1, 0x42, 0x24, 0x83, 0x21, 0x9e, 0x17, 0xf2, 0xe1, 0xe2, + 0x76, 0xcb, 0xcc, 0x4b, 0xf1, 0x4b, 0x4d, 0x2c, 0x02, 0xf9, 0x4a, 0x16, 0x87, 0x11, 0x10, 0x4f, + 0x4b, 0xc9, 0xe1, 0x92, 0x86, 0x38, 0xca, 0x89, 0x23, 0x0a, 0x1a, 0x6e, 0x49, 0x6c, 0xe0, 0x60, + 0x34, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0xc8, 0x90, 0x0b, 0x66, 0x76, 0x01, 0x00, 0x00, +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// DriverServiceClient is the client API for DriverService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type DriverServiceClient interface { + FindNearest(ctx context.Context, in *DriverLocationRequest, opts ...grpc.CallOption) (*DriverLocationResponse, error) +} + +type driverServiceClient struct { + cc *grpc.ClientConn +} + +func NewDriverServiceClient(cc *grpc.ClientConn) DriverServiceClient { + return &driverServiceClient{cc} +} + +func (c *driverServiceClient) FindNearest(ctx context.Context, in *DriverLocationRequest, opts ...grpc.CallOption) (*DriverLocationResponse, error) { + out := new(DriverLocationResponse) + err := c.cc.Invoke(ctx, "/driver.DriverService/FindNearest", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// DriverServiceServer is the server API for DriverService service. +type DriverServiceServer interface { + FindNearest(context.Context, *DriverLocationRequest) (*DriverLocationResponse, error) +} + +// UnimplementedDriverServiceServer can be embedded to have forward compatible implementations. +type UnimplementedDriverServiceServer struct { +} + +func (*UnimplementedDriverServiceServer) FindNearest(ctx context.Context, req *DriverLocationRequest) (*DriverLocationResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method FindNearest not implemented") +} + +func RegisterDriverServiceServer(s *grpc.Server, srv DriverServiceServer) { + s.RegisterService(&_DriverService_serviceDesc, srv) +} + +func _DriverService_FindNearest_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DriverLocationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DriverServiceServer).FindNearest(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/driver.DriverService/FindNearest", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DriverServiceServer).FindNearest(ctx, req.(*DriverLocationRequest)) + } + return interceptor(ctx, in, info, handler) +} + +var _DriverService_serviceDesc = grpc.ServiceDesc{ + ServiceName: "driver.DriverService", + HandlerType: (*DriverServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "FindNearest", + Handler: _DriverService_FindNearest_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "examples/hotrod/services/driver/driver.proto", +} diff --git a/services/driver/driver.proto b/services/driver/driver.proto new file mode 100644 index 0000000..0d9d98d --- /dev/null +++ b/services/driver/driver.proto @@ -0,0 +1,35 @@ +// Copyright (c) 2020 The Jaeger Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax="proto3"; +package driver; + +option go_package = "driver"; + +message DriverLocationRequest { + string location = 1; +} + +message DriverLocation { + string driverID = 1; + string location = 2; +} + +message DriverLocationResponse { + repeated DriverLocation locations = 1; +} + +service DriverService { + rpc FindNearest(DriverLocationRequest) returns (DriverLocationResponse); +} diff --git a/services/driver/empty_test.go b/services/driver/empty_test.go new file mode 100644 index 0000000..f1723a0 --- /dev/null +++ b/services/driver/empty_test.go @@ -0,0 +1,25 @@ +// Copyright (c) 2024 The Jaeger Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package driver + +import ( + "testing" + + "github.com/jaegertracing/jaeger/pkg/testutils" +) + +func TestMain(m *testing.M) { + testutils.VerifyGoLeaks(m) +} diff --git a/services/driver/interface.go b/services/driver/interface.go new file mode 100644 index 0000000..96c8cfc --- /dev/null +++ b/services/driver/interface.go @@ -0,0 +1,31 @@ +// Copyright (c) 2019 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package driver + +import ( + "context" +) + +// Driver describes a driver and the current car location. +type Driver struct { + DriverID string + Location string +} + +// Interface exposed by the Driver service. +type Interface interface { + FindNearest(ctx context.Context, location string) ([]Driver, error) +} diff --git a/services/driver/redis.go b/services/driver/redis.go new file mode 100644 index 0000000..004d4c1 --- /dev/null +++ b/services/driver/redis.go @@ -0,0 +1,112 @@ +// Copyright (c) 2019 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package driver + +import ( + "context" + "errors" + "fmt" + "math/rand" + "sync" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" + "go.uber.org/zap" + + "github.com/jaegertracing/jaeger/examples/hotrod/pkg/delay" + "github.com/jaegertracing/jaeger/examples/hotrod/pkg/log" + "github.com/jaegertracing/jaeger/examples/hotrod/pkg/tracing" + "github.com/jaegertracing/jaeger/examples/hotrod/services/config" + "github.com/jaegertracing/jaeger/pkg/metrics" +) + +// Redis is a simulator of remote Redis cache +type Redis struct { + tracer trace.Tracer // simulate redis as a separate process + logger log.Factory + errorSimulator +} + +func newRedis(otelExporter string, metricsFactory metrics.Factory, logger log.Factory) *Redis { + tp := tracing.InitOTEL("redis-manual", otelExporter, metricsFactory, logger) + return &Redis{ + tracer: tp.Tracer("redis-manual"), + logger: logger, + } +} + +// FindDriverIDs finds IDs of drivers who are near the location. +func (r *Redis) FindDriverIDs(ctx context.Context, location string) []string { + ctx, span := r.tracer.Start(ctx, "FindDriverIDs", trace.WithSpanKind(trace.SpanKindClient)) + span.SetAttributes(attribute.Key("param.driver.location").String(location)) + defer span.End() + + // simulate RPC delay + delay.Sleep(config.RedisFindDelay, config.RedisFindDelayStdDev) + + drivers := make([]string, 10) + for i := range drivers { + // #nosec + drivers[i] = fmt.Sprintf("T7%05dC", rand.Int()%100000) + } + r.logger.For(ctx).Info("Found drivers", zap.Strings("drivers", drivers)) + return drivers +} + +// GetDriver returns driver and the current car location +func (r *Redis) GetDriver(ctx context.Context, driverID string) (Driver, error) { + ctx, span := r.tracer.Start(ctx, "GetDriver", trace.WithSpanKind(trace.SpanKindClient)) + span.SetAttributes(attribute.Key("param.driverID").String(driverID)) + defer span.End() + + // simulate RPC delay + delay.Sleep(config.RedisGetDelay, config.RedisGetDelayStdDev) + if err := r.checkError(); err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, "An error occurred") + r.logger.For(ctx).Error("redis timeout", zap.String("driver_id", driverID), zap.Error(err)) + return Driver{}, err + } + + r.logger.For(ctx).Info("Got driver's ID", zap.String("driverID", driverID)) + + // #nosec + return Driver{ + DriverID: driverID, + Location: fmt.Sprintf("%d,%d", rand.Int()%1000, rand.Int()%1000), + }, nil +} + +var errTimeout = errors.New("redis timeout") + +type errorSimulator struct { + sync.Mutex + countTillError int +} + +func (es *errorSimulator) checkError() error { + es.Lock() + es.countTillError-- + if es.countTillError > 0 { + es.Unlock() + return nil + } + es.countTillError = 5 + es.Unlock() + delay.Sleep(2*config.RedisGetDelay, 0) // add more delay for "timeout" + return errTimeout +} diff --git a/services/driver/server.go b/services/driver/server.go new file mode 100644 index 0000000..9773ab5 --- /dev/null +++ b/services/driver/server.go @@ -0,0 +1,110 @@ +// Copyright (c) 2019 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package driver + +import ( + "context" + "encoding/json" + "net" + + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" + "go.uber.org/zap" + "google.golang.org/grpc" + + "github.com/jaegertracing/jaeger/examples/hotrod/pkg/log" + "github.com/jaegertracing/jaeger/examples/hotrod/pkg/tracing" + "github.com/jaegertracing/jaeger/pkg/metrics" +) + +// Server implements jaeger-demo-frontend service +type Server struct { + hostPort string + logger log.Factory + redis *Redis + server *grpc.Server +} + +var _ DriverServiceServer = (*Server)(nil) + +// NewServer creates a new driver.Server +func NewServer(hostPort string, otelExporter string, metricsFactory metrics.Factory, logger log.Factory) *Server { + tracerProvider := tracing.InitOTEL("driver", otelExporter, metricsFactory, logger) + server := grpc.NewServer( + grpc.StatsHandler(otelgrpc.NewServerHandler(otelgrpc.WithTracerProvider(tracerProvider))), + ) + return &Server{ + hostPort: hostPort, + logger: logger, + server: server, + redis: newRedis(otelExporter, metricsFactory, logger), + } +} + +// Run starts the Driver server +func (s *Server) Run() error { + lis, err := net.Listen("tcp", s.hostPort) + if err != nil { + s.logger.Bg().Fatal("Unable to create http listener", zap.Error(err)) + } + RegisterDriverServiceServer(s.server, s) + s.logger.Bg().Info("Starting", zap.String("address", s.hostPort), zap.String("type", "gRPC")) + err = s.server.Serve(lis) + if err != nil { + s.logger.Bg().Fatal("Unable to start gRPC server", zap.Error(err)) + } + return err +} + +// FindNearest implements gRPC driver interface +func (s *Server) FindNearest(ctx context.Context, location *DriverLocationRequest) (*DriverLocationResponse, error) { + s.logger.For(ctx).Info("Searching for nearby drivers", zap.String("location", location.Location)) + driverIDs := s.redis.FindDriverIDs(ctx, location.Location) + + locations := make([]*DriverLocation, len(driverIDs)) + for i, driverID := range driverIDs { + var drv Driver + var err error + for i := 0; i < 3; i++ { + drv, err = s.redis.GetDriver(ctx, driverID) + if err == nil { + break + } + s.logger.For(ctx).Error("Retrying GetDriver after error", zap.Int("retry_no", i+1), zap.Error(err)) + } + if err != nil { + s.logger.For(ctx).Error("Failed to get driver after 3 attempts", zap.Error(err)) + return nil, err + } + locations[i] = &DriverLocation{ + DriverID: drv.DriverID, + Location: drv.Location, + } + } + s.logger.For(ctx).Info( + "Search successful", + zap.Int("driver_count", len(locations)), + zap.String("locations", toJSON(locations)), + ) + return &DriverLocationResponse{Locations: locations}, nil +} + +func toJSON(v any) string { + str, err := json.Marshal(v) + if err != nil { + return err.Error() + } + return string(str) +} diff --git a/services/frontend/best_eta.go b/services/frontend/best_eta.go new file mode 100644 index 0000000..58c6bdb --- /dev/null +++ b/services/frontend/best_eta.go @@ -0,0 +1,147 @@ +// Copyright (c) 2019 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package frontend + +import ( + "context" + "errors" + "math" + "sync" + "time" + + "go.opentelemetry.io/otel/baggage" + "go.opentelemetry.io/otel/trace" + "go.uber.org/zap" + + "github.com/jaegertracing/jaeger/examples/hotrod/pkg/log" + "github.com/jaegertracing/jaeger/examples/hotrod/pkg/pool" + "github.com/jaegertracing/jaeger/examples/hotrod/services/config" + "github.com/jaegertracing/jaeger/examples/hotrod/services/customer" + "github.com/jaegertracing/jaeger/examples/hotrod/services/driver" + "github.com/jaegertracing/jaeger/examples/hotrod/services/route" +) + +type bestETA struct { + customer customer.Interface + driver driver.Interface + route route.Interface + pool *pool.Pool + logger log.Factory +} + +// Response contains ETA for a trip. +type Response struct { + Driver string + ETA time.Duration +} + +func newBestETA(tracer trace.TracerProvider, logger log.Factory, options ConfigOptions) *bestETA { + return &bestETA{ + customer: customer.NewClient( + tracer, + logger.With(zap.String("component", "customer_client")), + options.CustomerHostPort, + ), + driver: driver.NewClient( + tracer, + logger.With(zap.String("component", "driver_client")), + options.DriverHostPort, + ), + route: route.NewClient( + tracer, + logger.With(zap.String("component", "route_client")), + options.RouteHostPort, + ), + pool: pool.New(config.RouteWorkerPoolSize), + logger: logger, + } +} + +func (eta *bestETA) Get(ctx context.Context, customerID int) (*Response, error) { + customer, err := eta.customer.Get(ctx, customerID) + if err != nil { + return nil, err + } + eta.logger.For(ctx).Info("Found customer", zap.Any("customer", customer)) + + m, err := baggage.NewMember("customer", customer.Name) + if err != nil { + eta.logger.For(ctx).Error("cannot create baggage member", zap.Error(err)) + } + bag := baggage.FromContext(ctx) + bag, err = bag.SetMember(m) + if err != nil { + eta.logger.For(ctx).Error("cannot set baggage member", zap.Error(err)) + } + ctx = baggage.ContextWithBaggage(ctx, bag) + + drivers, err := eta.driver.FindNearest(ctx, customer.Location) + if err != nil { + return nil, err + } + eta.logger.For(ctx).Info("Found drivers", zap.Any("drivers", drivers)) + + results := eta.getRoutes(ctx, customer, drivers) + eta.logger.For(ctx).Info("Found routes", zap.Any("routes", results)) + + resp := &Response{ETA: math.MaxInt64} + for _, result := range results { + if result.err != nil { + return nil, err + } + if result.route.ETA < resp.ETA { + resp.ETA = result.route.ETA + resp.Driver = result.driver + } + } + if resp.Driver == "" { + return nil, errors.New("no routes found") + } + + eta.logger.For(ctx).Info("Dispatch successful", zap.String("driver", resp.Driver), zap.String("eta", resp.ETA.String())) + return resp, nil +} + +type routeResult struct { + driver string + route *route.Route + err error +} + +// getRoutes calls Route service for each (customer, driver) pair +func (eta *bestETA) getRoutes(ctx context.Context, customer *customer.Customer, drivers []driver.Driver) []routeResult { + results := make([]routeResult, 0, len(drivers)) + wg := sync.WaitGroup{} + routesLock := sync.Mutex{} + for _, dd := range drivers { + wg.Add(1) + driver := dd // capture loop var + // Use worker pool to (potentially) execute requests in parallel + eta.pool.Execute(func() { + route, err := eta.route.FindRoute(ctx, driver.Location, customer.Location) + routesLock.Lock() + results = append(results, routeResult{ + driver: driver.DriverID, + route: route, + err: err, + }) + routesLock.Unlock() + wg.Done() + }) + } + wg.Wait() + return results +} diff --git a/services/frontend/empty_test.go b/services/frontend/empty_test.go new file mode 100644 index 0000000..81450b7 --- /dev/null +++ b/services/frontend/empty_test.go @@ -0,0 +1,25 @@ +// Copyright (c) 2024 The Jaeger Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package frontend + +import ( + "testing" + + "github.com/jaegertracing/jaeger/pkg/testutils" +) + +func TestMain(m *testing.M) { + testutils.VerifyGoLeaks(m) +} diff --git a/services/frontend/server.go b/services/frontend/server.go new file mode 100644 index 0000000..7b451d9 --- /dev/null +++ b/services/frontend/server.go @@ -0,0 +1,143 @@ +// Copyright (c) 2019 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package frontend + +import ( + "embed" + "encoding/json" + "expvar" + "net/http" + "path" + "strconv" + "time" + + "github.com/prometheus/client_golang/prometheus/promhttp" + "go.opentelemetry.io/otel/trace" + "go.uber.org/zap" + + "github.com/jaegertracing/jaeger/examples/hotrod/pkg/httperr" + "github.com/jaegertracing/jaeger/examples/hotrod/pkg/log" + "github.com/jaegertracing/jaeger/examples/hotrod/pkg/tracing" + "github.com/jaegertracing/jaeger/pkg/httpfs" +) + +//go:embed web_assets/* +var assetFS embed.FS + +// Server implements jaeger-demo-frontend service +type Server struct { + hostPort string + tracer trace.TracerProvider + logger log.Factory + bestETA *bestETA + assetFS http.FileSystem + basepath string + jaegerUI string +} + +// ConfigOptions used to make sure service clients +// can find correct server ports +type ConfigOptions struct { + FrontendHostPort string + DriverHostPort string + CustomerHostPort string + RouteHostPort string + Basepath string + JaegerUI string +} + +// NewServer creates a new frontend.Server +func NewServer(options ConfigOptions, tracer trace.TracerProvider, logger log.Factory) *Server { + return &Server{ + hostPort: options.FrontendHostPort, + tracer: tracer, + logger: logger, + bestETA: newBestETA(tracer, logger, options), + assetFS: httpfs.PrefixedFS("web_assets", http.FS(assetFS)), + basepath: options.Basepath, + jaegerUI: options.JaegerUI, + } +} + +// Run starts the frontend server +func (s *Server) Run() error { + mux := s.createServeMux() + s.logger.Bg().Info("Starting", zap.String("address", "http://"+path.Join(s.hostPort, s.basepath))) + server := &http.Server{ + Addr: s.hostPort, + Handler: mux, + ReadHeaderTimeout: 3 * time.Second, + } + return server.ListenAndServe() +} + +func (s *Server) createServeMux() http.Handler { + mux := tracing.NewServeMux(true, s.tracer, s.logger) + p := path.Join("/", s.basepath) + mux.Handle(p, http.StripPrefix(p, http.FileServer(s.assetFS))) + mux.Handle(path.Join(p, "/dispatch"), http.HandlerFunc(s.dispatch)) + mux.Handle(path.Join(p, "/config"), http.HandlerFunc(s.config)) + mux.Handle(path.Join(p, "/debug/vars"), expvar.Handler()) // expvar + mux.Handle(path.Join(p, "/metrics"), promhttp.Handler()) // Prometheus + return mux +} + +func (s *Server) config(w http.ResponseWriter, r *http.Request) { + config := map[string]string{ + "jaeger": s.jaegerUI, + } + s.writeResponse(config, w, r) +} + +func (s *Server) dispatch(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + s.logger.For(ctx).Info("HTTP request received", zap.String("method", r.Method), zap.Stringer("url", r.URL)) + if err := r.ParseForm(); httperr.HandleError(w, err, http.StatusBadRequest) { + s.logger.For(ctx).Error("bad request", zap.Error(err)) + return + } + + customer := r.Form.Get("customer") + if customer == "" { + http.Error(w, "Missing required 'customer' parameter", http.StatusBadRequest) + return + } + customerID, err := strconv.Atoi(customer) + if err != nil { + http.Error(w, "Parameter 'customer' is not an integer", http.StatusBadRequest) + return + } + + // TODO distinguish between user errors (such as invalid customer ID) and server failures + response, err := s.bestETA.Get(ctx, customerID) + if httperr.HandleError(w, err, http.StatusInternalServerError) { + s.logger.For(ctx).Error("request failed", zap.Error(err)) + return + } + + s.writeResponse(response, w, r) +} + +func (s *Server) writeResponse(response interface{}, w http.ResponseWriter, r *http.Request) { + data, err := json.Marshal(response) + if httperr.HandleError(w, err, http.StatusInternalServerError) { + s.logger.For(r.Context()).Error("cannot marshal response", zap.Error(err)) + return + } + + w.Header().Set("Content-Type", "application/json") + w.Write(data) +} diff --git a/services/frontend/web_assets/code.jquery.com_jquery-3.7.0.min.js b/services/frontend/web_assets/code.jquery.com_jquery-3.7.0.min.js new file mode 100644 index 0000000..e7e29d5 --- /dev/null +++ b/services/frontend/web_assets/code.jquery.com_jquery-3.7.0.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.7.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(ie,e){"use strict";var oe=[],r=Object.getPrototypeOf,ae=oe.slice,g=oe.flat?function(e){return oe.flat.call(e)}:function(e){return oe.concat.apply([],e)},s=oe.push,se=oe.indexOf,n={},i=n.toString,ue=n.hasOwnProperty,o=ue.toString,a=o.call(Object),le={},v=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},y=function(e){return null!=e&&e===e.window},C=ie.document,u={type:!0,src:!0,nonce:!0,noModule:!0};function m(e,t,n){var r,i,o=(n=n||C).createElement("script");if(o.text=e,t)for(r in u)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[i.call(e)]||"object":typeof e}var t="3.7.0",l=/HTML$/i,ce=function(e,t){return new ce.fn.init(e,t)};function c(e){var t=!!e&&"length"in e&&e.length,n=x(e);return!v(e)&&!y(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+ge+")"+ge+"*"),x=new RegExp(ge+"|>"),j=new RegExp(g),A=new RegExp("^"+t+"$"),D={ID:new RegExp("^#("+t+")"),CLASS:new RegExp("^\\.("+t+")"),TAG:new RegExp("^("+t+"|[*])"),ATTR:new RegExp("^"+p),PSEUDO:new RegExp("^"+g),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+ge+"*(even|odd|(([+-]|)(\\d*)n|)"+ge+"*(?:([+-]|)"+ge+"*(\\d+)|))"+ge+"*\\)|)","i"),bool:new RegExp("^(?:"+f+")$","i"),needsContext:new RegExp("^"+ge+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+ge+"*((?:-\\d)?\\d*)"+ge+"*\\)|)(?=[^-]|$)","i")},N=/^(?:input|select|textarea|button)$/i,q=/^h\d$/i,L=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,H=/[+~]/,O=new RegExp("\\\\[\\da-fA-F]{1,6}"+ge+"?|\\\\([^\\r\\n\\f])","g"),P=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},R=function(){V()},M=J(function(e){return!0===e.disabled&&fe(e,"fieldset")},{dir:"parentNode",next:"legend"});try{k.apply(oe=ae.call(ye.childNodes),ye.childNodes),oe[ye.childNodes.length].nodeType}catch(e){k={apply:function(e,t){me.apply(e,ae.call(t))},call:function(e){me.apply(e,ae.call(arguments,1))}}}function I(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(V(e),e=e||T,C)){if(11!==p&&(u=L.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return k.call(n,a),n}else if(f&&(a=f.getElementById(i))&&I.contains(e,a)&&a.id===i)return k.call(n,a),n}else{if(u[2])return k.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&e.getElementsByClassName)return k.apply(n,e.getElementsByClassName(i)),n}if(!(h[t+" "]||d&&d.test(t))){if(c=t,f=e,1===p&&(x.test(t)||m.test(t))){(f=H.test(t)&&z(e.parentNode)||e)==e&&le.scope||((s=e.getAttribute("id"))?s=ce.escapeSelector(s):e.setAttribute("id",s=S)),o=(l=Y(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+Q(l[o]);c=l.join(",")}try{return k.apply(n,f.querySelectorAll(c)),n}catch(e){h(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return re(t.replace(ve,"$1"),e,n,r)}function W(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function F(e){return e[S]=!0,e}function $(e){var t=T.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function B(t){return function(e){return fe(e,"input")&&e.type===t}}function _(t){return function(e){return(fe(e,"input")||fe(e,"button"))&&e.type===t}}function X(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&M(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function U(a){return F(function(o){return o=+o,F(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function z(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}function V(e){var t,n=e?e.ownerDocument||e:ye;return n!=T&&9===n.nodeType&&n.documentElement&&(r=(T=n).documentElement,C=!ce.isXMLDoc(T),i=r.matches||r.webkitMatchesSelector||r.msMatchesSelector,ye!=T&&(t=T.defaultView)&&t.top!==t&&t.addEventListener("unload",R),le.getById=$(function(e){return r.appendChild(e).id=ce.expando,!T.getElementsByName||!T.getElementsByName(ce.expando).length}),le.disconnectedMatch=$(function(e){return i.call(e,"*")}),le.scope=$(function(){return T.querySelectorAll(":scope")}),le.cssHas=$(function(){try{return T.querySelector(":has(*,:jqfake)"),!1}catch(e){return!0}}),le.getById?(b.filter.ID=function(e){var t=e.replace(O,P);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&C){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(O,P);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&C){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):t.querySelectorAll(e)},b.find.CLASS=function(e,t){if("undefined"!=typeof t.getElementsByClassName&&C)return t.getElementsByClassName(e)},d=[],$(function(e){var t;r.appendChild(e).innerHTML="",e.querySelectorAll("[selected]").length||d.push("\\["+ge+"*(?:value|"+f+")"),e.querySelectorAll("[id~="+S+"-]").length||d.push("~="),e.querySelectorAll("a#"+S+"+*").length||d.push(".#.+[+~]"),e.querySelectorAll(":checked").length||d.push(":checked"),(t=T.createElement("input")).setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),r.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&d.push(":enabled",":disabled"),(t=T.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||d.push("\\["+ge+"*name"+ge+"*="+ge+"*(?:''|\"\")")}),le.cssHas||d.push(":has"),d=d.length&&new RegExp(d.join("|")),l=function(e,t){if(e===t)return a=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!le.sortDetached&&t.compareDocumentPosition(e)===n?e===T||e.ownerDocument==ye&&I.contains(ye,e)?-1:t===T||t.ownerDocument==ye&&I.contains(ye,t)?1:o?se.call(o,e)-se.call(o,t):0:4&n?-1:1)}),T}for(e in I.matches=function(e,t){return I(e,null,null,t)},I.matchesSelector=function(e,t){if(V(e),C&&!h[t+" "]&&(!d||!d.test(t)))try{var n=i.call(e,t);if(n||le.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){h(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(O,P),e[3]=(e[3]||e[4]||e[5]||"").replace(O,P),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||I.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&I.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return D.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&j.test(n)&&(t=Y(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(O,P).toLowerCase();return"*"===e?function(){return!0}:function(e){return fe(e,t)}},CLASS:function(e){var t=s[e+" "];return t||(t=new RegExp("(^|"+ge+")"+e+"("+ge+"|$)"))&&s(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=I.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function T(e,n,r){return v(n)?ce.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?ce.grep(e,function(e){return e===n!==r}):"string"!=typeof n?ce.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(ce.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||k,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:S.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof ce?t[0]:t,ce.merge(this,ce.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:C,!0)),w.test(r[1])&&ce.isPlainObject(t))for(r in t)v(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=C.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):v(e)?void 0!==n.ready?n.ready(e):e(ce):ce.makeArray(e,this)}).prototype=ce.fn,k=ce(C);var E=/^(?:parents|prev(?:Until|All))/,j={children:!0,contents:!0,next:!0,prev:!0};function A(e,t){while((e=e[t])&&1!==e.nodeType);return e}ce.fn.extend({has:function(e){var t=ce(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,Ce=/^$|^module$|\/(?:java|ecma)script/i;xe=C.createDocumentFragment().appendChild(C.createElement("div")),(be=C.createElement("input")).setAttribute("type","radio"),be.setAttribute("checked","checked"),be.setAttribute("name","t"),xe.appendChild(be),le.checkClone=xe.cloneNode(!0).cloneNode(!0).lastChild.checked,xe.innerHTML="",le.noCloneChecked=!!xe.cloneNode(!0).lastChild.defaultValue,xe.innerHTML="",le.option=!!xe.lastChild;var ke={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function Se(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&fe(e,t)?ce.merge([e],n):n}function Ee(e,t){for(var n=0,r=e.length;n",""]);var je=/<|&#?\w+;/;function Ae(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function Me(e,t){return fe(e,"table")&&fe(11!==t.nodeType?t:t.firstChild,"tr")&&ce(e).children("tbody")[0]||e}function Ie(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function We(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Fe(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(_.hasData(e)&&(s=_.get(e).events))for(i in _.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),C.head.appendChild(r[0])},abort:function(){i&&i()}}});var Jt,Kt=[],Zt=/(=)\?(?=&|$)|\?\?/;ce.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Kt.pop()||ce.expando+"_"+jt.guid++;return this[e]=!0,e}}),ce.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Zt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Zt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=v(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Zt,"$1"+r):!1!==e.jsonp&&(e.url+=(At.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||ce.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=ie[r],ie[r]=function(){o=arguments},n.always(function(){void 0===i?ce(ie).removeProp(r):ie[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Kt.push(r)),o&&v(i)&&i(o[0]),o=i=void 0}),"script"}),le.createHTMLDocument=((Jt=C.implementation.createHTMLDocument("").body).innerHTML="
",2===Jt.childNodes.length),ce.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(le.createHTMLDocument?((r=(t=C.implementation.createHTMLDocument("")).createElement("base")).href=C.location.href,t.head.appendChild(r)):t=C),o=!n&&[],(i=w.exec(e))?[t.createElement(i[1])]:(i=Ae([e],t,o),o&&o.length&&ce(o).remove(),ce.merge([],i.childNodes)));var r,i,o},ce.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(ce.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},ce.expr.pseudos.animated=function(t){return ce.grep(ce.timers,function(e){return t===e.elem}).length},ce.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=ce.css(e,"position"),c=ce(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=ce.css(e,"top"),u=ce.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),v(t)&&(t=t.call(e,n,ce.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},ce.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){ce.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===ce.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===ce.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=ce(e).offset()).top+=ce.css(e,"borderTopWidth",!0),i.left+=ce.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-ce.css(r,"marginTop",!0),left:t.left-i.left-ce.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===ce.css(e,"position"))e=e.offsetParent;return e||J})}}),ce.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;ce.fn[t]=function(e){return R(this,function(e,t,n){var r;if(y(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),ce.each(["top","left"],function(e,n){ce.cssHooks[n]=Ye(le.pixelPosition,function(e,t){if(t)return t=Ge(e,n),_e.test(t)?ce(e).position()[n]+"px":t})}),ce.each({Height:"height",Width:"width"},function(a,s){ce.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){ce.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return R(this,function(e,t,n){var r;return y(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?ce.css(e,t,i):ce.style(e,t,n,i)},s,n?e:void 0,n)}})}),ce.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){ce.fn[t]=function(e){return this.on(t,e)}}),ce.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),ce.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){ce.fn[n]=function(e,t){return 06>1F*b~ev0$O7(HB7B?*E%}xx@krQS;tgf9_g)edo+MGv7CJr=7ir$Lqy=k0&X~ zBdfui)rZHk=J9wM8iIQRDITwk(#*`-pX>2>C**iM1A2$DP!2&lnNEKVC2GqWOEg*t z=pkBO%v)cqfd;~>%Bd?-fA)EY--}j4GVk} zpL@PCI0&{@mWYasK-BD67-nRJNsbAG#T^)F=7#AZ?;|{HI$TDM#QbqS9c37*mqAz8 z8_|?EWa?xntIh>a2zABMj{Hf}S3yDjGAwP9;pDOo zO4_%etW!_8OSs2f%P8N6m2Cxd4dS7u`%m~gZtBSYW!hM*&8&y3Hy9 z;%S3|MjK>R+90jOC(B2mOTlybSsC>uI`}kg25cOEfR6DQq@}J$*19sR&)I^!4OLja z@)#oK1Hm)eusvzUOZizD4TLb?3~b2^z=0B9f_Dd&&YcZWX~4|nH158lpIpwhLfqU8 zoIMtUv!Bl+I}hKHjhpY3<2){aME?gpItG8t!2rr^u!)d-$>9$`~Y z;#R)VYxA?RZk1T#`?4^6TNaJ8gbRf}IJru%TX~<2{b_G1!ALxV)WC z3Oz_<^Na?;nZV|!u6{`7^{y{cUrO=S_8`X2>fDpGTIQ4UUBdb9N#3{iZoR-L zWAD;8o3HN_sN-JYbMx|x`Coa@%D7EsyfC-FG7swmpCl;4)l3@N>Wa`&lf@)^30$Fh zz9;>oB2pf<#B6b$tB5ojyXhpC>P7-3_B`SYT&XV*?_swl_% zlY?P8#1sR?lgRJ!SEFw=^!2PU(#er)!-EI*W5fC!_z4&Pc2$k+7F4e=Az{;@&B4V{rQ;&m{r@Iyb*uW60Hhx~|; zm;Be{8o|W$7zW8UW2j>*9Nllg*kUg%ZI2;*eiN*y?Ds_LJL1-z{gO# zR`d~TMz8)2ST}9vOa9roJ-OBc=f1y(J^LQg-iQxz*)|N4drI~R21qocuh?U1gGRhJ z@IK)Y;opScIrJvW(2LR-dJkmpQ$ES35E=9o{luS;{}yiVmgpbTsecy3cNMX?bm;-o z3urGE1`M_3Lq+c?q?B7AA@^^HNnMAiWG(fRWHV*wji zz`~K@903?u0($0r=$Zl=Mu4h5psWihY62=`S=unWi^!N3{7U~U28KJOW;l@(Msvs4 zh>YjM&XxAw{r1v6cpsLo{0SQ>?xAA)BYd>48C3_LV&{=I?5^fx_fbB!f6AvcpmIOx zxxnUB06zX#aVgupTmFS{oX8qMwccLJ8q4Zz~)Nw9PL2nJ>}4$P?jTHwZ3(flbr^37tXE7ipG38ffjnS-iKUu@6u z#m-zW?8^1vusheC>>m)`d$=Uf{||`K-g=-UpZ3>B@l%mjkNvY4&J_-$IpjxtxF`@u zHc!TpEx}|bl4W3Ngm!y$a{!KR34nv$dTw7YF3bG4VQFYI_Z=W?*Phza#Aw*NZpEin zp*VFY0;diV4uuPFklC3X_-bz`eN!E#F#|}-gXYwm_&(40Pt{!*XMMRgqC>VLDrz~- zom`CbrmRYT36h`~L2V7x@2@&Ug}{XnE5Jui{DQF|V;u zOQOD5Z3^|56OE-BzyGKD&1?M%VmrabN-+y#Z!FQ=KzW;9oBu7WlW)lf#Y!;?+r3z$ z`7M>(>3Do6WmC)@edfQXU3*&p|L*?&x_0fsSiMVp+l>usxRs{{rklKrKEe37o8*Jx z-t)T4pYm5%M0=c-%1H2*fra)UD9ejOT|o>U=Hl2iTb}Ckdfu%&^<-L4#{05xHj#j; z{2(aFieb2(7*+lfO@69}2T*)#*+T_7dRZ)h6y*<*?RwFVzj5s$p1p518 z8I?UTFaX-x+ORj4!pY@&sL3+!88VGgG*boIYVxoo9*pt1aWV7Y?d3t7pcfJrE$2_oFzrQT+QUelM698DqScC#Tcqtv-@W#UVF(}B( zrLnMz%ALyb&}_cY(w;*6^QXkKKC3@79r}lM2sP=KZNC8pH0L-|I~frd?TR@OGm)K@ zf$Yo-;?`!uZ8Y)Iq;Z%IwMMVLo4K}`;X1F&_zNO@@SeyC^7{*ViyWr@??e2!B@C&b zJKEcl=HLN$R;K9?jt6&i90M`Q4B}piE9UrG#?iKOwT#d0xEAuaFL1h~x63O`hV6(E zFfuSCeP9@A6mIZz9}R0O3m6+3z~0UtGP2b6eNIvQ=V^RKbs7J;3~P9eu7+X&;*m*nmuo;@(Hf|!O{9J@lC+E=(4#p+Ur(3Q1?+5HprRUzJ_CM)n9OyUn4HI_ zOGkGZ|IGqDB+p5Jj>#?3z5fkyxrfBFehXF24Cv^E(RiK!ef>$$(n*A&>0Vek5O1sa z8=Vt)3=8{*Ft=}ngzm9{2-91XLYezlV2Eu*91A6x{)%!7Ilv-eDbqh*r<c&|l&{oXof3a>mduibXo?z_6IyjBJ0e8PzL@j!Z^_%$cn9>Fj$%pOzgtQjwcf9^gb71 zZrNr0^`uqoT{!{HqyCB6(U&pA@)2|_+At#y#76@MKL@H$0aXWpph!N%Q~(6n=xARVx;*O4@`V zer2T-IJ3&8%lO&eeV{z*>j7|cF_NZM;OyB7WAZ08xE1{e zG41COwPPdIqmJTXIt;T7f_l+e6m|~fWl(y5mWSa1mC*z_^(U}&YK5C$8%BAz!C)Aj zTTy(9KBDAfU<=p&($Z(}Rf@wa<8LU@#MvxYY}s@kO-(J>yZ;e%hw!PNGTnxu72+~2 zG@e@Me{G>UG?51Lm^7LDghm>V56S0a!taEp=fL6=qW8ci^rm|T*1jyxe&Pa6hv_;Z zgIYWEoxY^+^rrtwOyWBnS?co2_=Wq!@5>@^=ETo<@Sp{$S$t0SGP5VG#E>{^jaEpL z?juh9M@*)P=BmdukKQF+=O&?%IQ1riKw}z6Iup~HnEumW2#T-25L}v2BVv~NJ8|-& z(hPjkk@%c$)KC0AbhWqQi89~U>z!m(YSAtUcTN~)fsxC;0n0|<=;CWP?e zG`VWFw!TCc z*UHA=Xl^nRV~)Wu13_gytfUT zcJVoE+{s7z4qzkAF=g9;;;lgb79jT@q|I&wa>@y$sj;-eO1h@Bs71daIU*f5@(sG( z{+-o>rCl!_hWVk{&^Bx)4ebo|`D&UUYG|JLiPny9Ff}*{v%*&+W?nWH#OGttl2Rlk zZ$k2lN~}ox80lF%ur_-))~(-*jNE-#mwOPavyLM*<1s$WqBh9^KBRuSBo&wy1GxF} zVPka`2S4zmZ^^Fr|6RrVVmjmgkLTG!S)~Tb>IdLtmyY0Rw-K}WK9(e&LD}+AC|&N1 zl9Z7sCKM$LP?+pO*RCj9KAJEFg(+?*Nbx{kvM1sq#z9N(5I4`ssQ?VpilF%p5S3`4 zb=4QQX?=Y$mOp!cyIcneGgm@hu?7KNL8x5ojmmZY_&9Ssc4UtwxD(v4lfxLYogjzO z+OdAwNZOy2&|E|DN(r<{X4yPtYq1Sq)86LK#@bcBlerGCwLVFEyb|QnIg0}&f!JRX zK=5lni|9j^;g2(kFJ2$yo{dLdsvC6mchj0e@k>*?)11RBYy0HcVYvPdIGa1AI#?IG z2%0)KFwrjr`zt1+x^gOxZk>Yat&=z$WwtVyu7hxt*~%cnJ!jc9fu3cj`qDn}Tbefo z^W`8pW=W&gKaE3cM!b3xrelEXr`4k@DFb&)-t1gMcjJEcy}QMRII=z&3le@n>_S?1mqudk>QLm-d7a|?nJ6iY zKvDiIvGxqbhG3N{hZn zS@{K&Z9I?CiVK9xDBXCKa0A8Vw@_Gm4Y}(tAb-tae3qY#FEib+JY)!Qm~V&M?uhBn z%mK7M+%31lujQ_|TIP+KGJn*R2NEVy{Q_{c+y}o_xZ_U6aMYIx_GR~o_pXf7$Kj># zcFuXDdcCGCZqu3B8+re`@xRNq`Md1D`^9s8pa&Y{bNOB~2LF + + + + HotROD - Rides On Demand + + + + + + + + +
+
+
+

Hot R.O.D.

+

🚗 Rides On Demand 🚗

+
+
+ Rachel's Floral Designs +
+
+ Trom Chocolatier +
+
+ Japanese Desserts +
+
+ Amazing Coffee Roasters +
+
+
Click on customer name above to order a car.
+
+
+
+ + + + + diff --git a/services/route/client.go b/services/route/client.go new file mode 100644 index 0000000..7c58f76 --- /dev/null +++ b/services/route/client.go @@ -0,0 +1,59 @@ +// Copyright (c) 2019 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package route + +import ( + "context" + "net/url" + + "go.opentelemetry.io/otel/trace" + "go.uber.org/zap" + + "github.com/jaegertracing/jaeger/examples/hotrod/pkg/log" + "github.com/jaegertracing/jaeger/examples/hotrod/pkg/tracing" +) + +// Client is a remote client that implements route.Interface +type Client struct { + logger log.Factory + client *tracing.HTTPClient + hostPort string +} + +// NewClient creates a new route.Client +func NewClient(tracer trace.TracerProvider, logger log.Factory, hostPort string) *Client { + return &Client{ + logger: logger, + client: tracing.NewHTTPClient(tracer), + hostPort: hostPort, + } +} + +// FindRoute implements route.Interface#FindRoute as an RPC +func (c *Client) FindRoute(ctx context.Context, pickup, dropoff string) (*Route, error) { + c.logger.For(ctx).Info("Finding route", zap.String("pickup", pickup), zap.String("dropoff", dropoff)) + + v := url.Values{} + v.Set("pickup", pickup) + v.Set("dropoff", dropoff) + url := "http://" + c.hostPort + "/route?" + v.Encode() + var route Route + if err := c.client.GetJSON(ctx, "/route", url, &route); err != nil { + c.logger.For(ctx).Error("Error getting route", zap.Error(err)) + return nil, err + } + return &route, nil +} diff --git a/services/route/empty_test.go b/services/route/empty_test.go new file mode 100644 index 0000000..ae53538 --- /dev/null +++ b/services/route/empty_test.go @@ -0,0 +1,25 @@ +// Copyright (c) 2024 The Jaeger Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package route + +import ( + "testing" + + "github.com/jaegertracing/jaeger/pkg/testutils" +) + +func TestMain(m *testing.M) { + testutils.VerifyGoLeaks(m) +} diff --git a/services/route/interface.go b/services/route/interface.go new file mode 100644 index 0000000..5bbfa63 --- /dev/null +++ b/services/route/interface.go @@ -0,0 +1,33 @@ +// Copyright (c) 2019 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package route + +import ( + "context" + "time" +) + +// Route describes a route between Pickup and Dropoff locations and expected time to arrival. +type Route struct { + Pickup string + Dropoff string + ETA time.Duration +} + +// Interface exposed by the Driver service. +type Interface interface { + FindRoute(ctx context.Context, pickup, dropoff string) (*Route, error) +} diff --git a/services/route/server.go b/services/route/server.go new file mode 100644 index 0000000..2cea77c --- /dev/null +++ b/services/route/server.go @@ -0,0 +1,123 @@ +// Copyright (c) 2019 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package route + +import ( + "context" + "encoding/json" + "math" + "math/rand" + "net/http" + "time" + + "go.opentelemetry.io/otel/trace" + "go.uber.org/zap" + + "github.com/jaegertracing/jaeger/examples/hotrod/pkg/delay" + "github.com/jaegertracing/jaeger/examples/hotrod/pkg/httperr" + "github.com/jaegertracing/jaeger/examples/hotrod/pkg/log" + "github.com/jaegertracing/jaeger/examples/hotrod/pkg/tracing" + "github.com/jaegertracing/jaeger/examples/hotrod/services/config" +) + +// Server implements Route service +type Server struct { + hostPort string + tracer trace.TracerProvider + logger log.Factory +} + +// NewServer creates a new route.Server +func NewServer(hostPort string, tracer trace.TracerProvider, logger log.Factory) *Server { + return &Server{ + hostPort: hostPort, + tracer: tracer, + logger: logger, + } +} + +// Run starts the Route server +func (s *Server) Run() error { + mux := s.createServeMux() + s.logger.Bg().Info("Starting", zap.String("address", "http://"+s.hostPort)) + server := &http.Server{ + Addr: s.hostPort, + Handler: mux, + ReadHeaderTimeout: 3 * time.Second, + } + return server.ListenAndServe() +} + +func (s *Server) createServeMux() http.Handler { + mux := tracing.NewServeMux(false, s.tracer, s.logger) + mux.Handle("/route", http.HandlerFunc(s.route)) + mux.Handle("/debug/vars", http.HandlerFunc(movedToFrontend)) + mux.Handle("/metrics", http.HandlerFunc(movedToFrontend)) + return mux +} + +func movedToFrontend(w http.ResponseWriter, r *http.Request) { + http.Error(w, "endpoint moved to the frontend service", http.StatusNotFound) +} + +func (s *Server) route(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + s.logger.For(ctx).Info("HTTP request received", zap.String("method", r.Method), zap.Stringer("url", r.URL)) + if err := r.ParseForm(); httperr.HandleError(w, err, http.StatusBadRequest) { + s.logger.For(ctx).Error("bad request", zap.Error(err)) + return + } + + pickup := r.Form.Get("pickup") + if pickup == "" { + http.Error(w, "Missing required 'pickup' parameter", http.StatusBadRequest) + return + } + + dropoff := r.Form.Get("dropoff") + if dropoff == "" { + http.Error(w, "Missing required 'dropoff' parameter", http.StatusBadRequest) + return + } + + response := computeRoute(ctx, pickup, dropoff) + + data, err := json.Marshal(response) + if httperr.HandleError(w, err, http.StatusInternalServerError) { + s.logger.For(ctx).Error("cannot marshal response", zap.Error(err)) + return + } + + w.Header().Set("Content-Type", "application/json") + w.Write(data) +} + +func computeRoute(ctx context.Context, pickup, dropoff string) *Route { + start := time.Now() + defer func() { + updateCalcStats(ctx, time.Since(start)) + }() + + // Simulate expensive calculation + delay.Sleep(config.RouteCalcDelay, config.RouteCalcDelayStdDev) + + eta := math.Max(2, rand.NormFloat64()*3+5) + return &Route{ + Pickup: pickup, + Dropoff: dropoff, + ETA: time.Duration(eta) * time.Minute, + } +} diff --git a/services/route/stats.go b/services/route/stats.go new file mode 100644 index 0000000..c5221f4 --- /dev/null +++ b/services/route/stats.go @@ -0,0 +1,53 @@ +// Copyright (c) 2019 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package route + +import ( + "context" + "expvar" + "time" + + "github.com/jaegertracing/jaeger/examples/hotrod/pkg/tracing" +) + +var ( + routeCalcByCustomer = expvar.NewMap("route.calc.by.customer.sec") + routeCalcBySession = expvar.NewMap("route.calc.by.session.sec") +) + +var stats = []struct { + expvar *expvar.Map + baggageKey string +}{ + { + expvar: routeCalcByCustomer, + baggageKey: "customer", + }, + { + expvar: routeCalcBySession, + baggageKey: "session", + }, +} + +func updateCalcStats(ctx context.Context, delay time.Duration) { + delaySec := float64(delay/time.Millisecond) / 1000.0 + for _, s := range stats { + key := tracing.BaggageItem(ctx, s.baggageKey) + if key != "" { + s.expvar.AddFloat(key, delaySec) + } + } +}