diff --git a/docker/Dockerfile b/docker/Dockerfile index 6a51e844..a62e5a38 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -37,7 +37,8 @@ RUN set -x \ && git clone https://github.com/opentracing/opentracing-cpp.git \ && cd opentracing-cpp \ && mkdir .build && cd .build \ - && cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF .. \ + && cmake -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_TESTING=OFF .. \ && make && make install \ && cd "$tempDir" \ ## Build zipkin-cpp-opentracing @@ -48,38 +49,32 @@ RUN set -x \ && cmake -DBUILD_SHARED_LIBS=1 -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF .. \ && make && make install \ && cd "$tempDir" \ +### Build Jaeger cpp-client + && git clone https://github.com/jaegertracing/cpp-client.git jaeger-cpp-client \ + && cd jaeger-cpp-client \ + && mkdir .build && cd .build \ + && cmake -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_TESTING=OFF \ + -DJAEGERTRACING_WITH_YAML_CPP=OFF .. \ + && make && make install \ + && export HUNTER_INSTALL_DIR=$(cat _3rdParty/Hunter/install-root-dir) \ + && cd "$tempDir" \ ### Build gRPC && git clone -b v1.4.x https://github.com/grpc/grpc \ && cd grpc \ && git submodule update --init \ && make HAS_SYSTEM_PROTOBUF=false && make install \ -# && mkdir .build && cd .build \ -# && cmake -DCMAKE_BUILD_TYPE=Release -DgRPC_ZLIB_PROVIDER=package .. \ && make && make install \ && cd "$tempDir" \ ### Build lightstep-tracer-cpp -# && apt-get install --no-install-recommends --no-install-suggests -y \ -# libprotobuf-dev \ -# protobuf-compiler \ && git clone https://github.com/lightstep/lightstep-tracer-cpp.git \ && cd lightstep-tracer-cpp \ && mkdir .build && cd .build \ && cmake -DBUILD_SHARED_LIBS=1 -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF .. \ && make && make install \ && cd "$tempDir" \ -### Build Jaeger cpp-client - && git clone https://github.com/jaegertracing/cpp-client.git jaeger-cpp-client \ - && cd jaeger-cpp-client \ - && mkdir .build && cd .build \ - && cmake -DCMAKE_BUILD_TYPE=Release \ - -DBUILD_TESTING=OFF \ - -DJAEGERTRACING_WITH_YAML_CPP=OFF .. \ - && make && make install \ - && export HUNTER_INSTALL_DIR=$(cat _3rdParty/Hunter/install-root-dir) \ - && cd "$tempDir" \ -### Get nginx-opentracing source +### Build nginx-opentracing modules && git clone https://github.com/opentracing-contrib/nginx-opentracing.git \ -## Build nginx-opentracing modules && NGINX_VERSION=`nginx -v 2>&1` && NGINX_VERSION=${NGINX_VERSION#*nginx/} \ && echo "deb-src http://nginx.org/packages/mainline/debian/ stretch nginx" >> /etc/apt/sources.list \ && apt-get update \ @@ -102,10 +97,11 @@ RUN set -x \ && cp objs/ngx_http_lightstep_module.so $NGINX_MODULES_PATH/ \ && cp objs/ngx_http_jaeger_module.so $NGINX_MODULES_PATH/ \ # if we have leftovers from building, let's purge them (including extra, unnecessary build deps) - && if [ -n "$tempDir" ]; then \ - apt-get purge -y --auto-remove \ - && rm -rf "$tempDir" /etc/apt/sources.list.d/temp.list; \ - fi + && rm -rf $HOME/.hunter \ + && if [ -n "$tempDir" ]; then \ + apt-get purge -y --auto-remove \ + && rm -rf "$tempDir" /etc/apt/sources.list.d/temp.list; \ + fi # forward request and error logs to docker log collector RUN ln -sf /dev/stdout /var/log/nginx/access.log \ diff --git a/example/hotrod/Dockerfile b/example/hotrod/Dockerfile new file mode 100644 index 00000000..aa7ebe78 --- /dev/null +++ b/example/hotrod/Dockerfile @@ -0,0 +1,52 @@ +FROM rnburn/nginx-opentracing + +WORKDIR /app +ADD . /app +RUN set -x \ +# new directory for storing sources and .deb files + && tempDir="$(mktemp -d)" \ + && chmod 777 "$tempDir" \ + \ +# save list of currently-installed packages so build dependencies can be cleanly removed later + && savedAptMark="$(apt-mark showmanual)" \ + \ +# set up go + && apt-get update \ + && apt-get install --no-install-recommends --no-install-suggests -y \ + git \ + ca-certificates \ + software-properties-common \ + wget \ + curl \ + && wget https://storage.googleapis.com/golang/go1.8.3.linux-amd64.tar.gz \ + && tar -xvf go1.8.3.linux-amd64.tar.gz \ + && mv go $tempDir \ + && export GOROOT=$tempDir/go \ + && export GOPATH=$tempDir/gopath \ + && mkdir -p $GOPATH/bin \ + && export PATH=$GOPATH/bin:$GOROOT/bin:$PATH \ +# install glide + && curl https://glide.sh/get | sh \ +# build the hotrod demo + && mkdir -p $GOPATH/src/github.com/rnburn/ \ + && mv /app/hotrod $GOPATH/src/github.com/rnburn/hotrod-docker \ + && cd $GOPATH/src/github.com/rnburn/hotrod-docker \ + && glide install \ + && go build -o hotrod \ + && mv hotrod /app/hotrod \ + && mkdir -p /app/services/frontend/web_assets \ + && cp services/frontend/web_assets/index.html /app/services/frontend/web_assets/index.html \ +# reset apt-mark's "manual" list so that "purge --auto-remove" will remove all build dependencies +# (which is done after we install the built packages so we don't have to redownload any overlapping dependencies) + && apt-mark showmanual | xargs apt-mark auto > /dev/null \ + && { [ -z "$savedAptMark" ] || apt-mark manual $savedAptMark; } \ + \ +# purge leftovers from building them (including extra, unnecessary build deps) + && apt-get purge -y --auto-remove \ + && rm -rf "$tempDir" + +EXPOSE 8080 + +STOPSIGNAL SIGTERM + +CMD ["/app/start.sh"] diff --git a/example/hotrod/README.md b/example/hotrod/README.md new file mode 100644 index 00000000..2e108f61 --- /dev/null +++ b/example/hotrod/README.md @@ -0,0 +1,14 @@ +An OpenTracing example demonstrating usage of the nginx-opentracing docker +image with jaeger. It builds on Jaeger's HotROD example (see blog post +[Take OpenTracing for a HotROD ride](https://medium.com/opentracing/take-opentracing-for-a-hotrod-ride-f6e3141f7941)), +but inserts nginx as a load balancer for the HotROD services. + +Use these commands to run: +```bash +docker build -t nginx-example-hotrod . +docker run -d -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 -p5775:5775/udp -p6831:6831/udp -p6832:6832/udp \ + -p5778:5778 -p16686:16686 -p14268:14268 -p9411:9411 --name jaeger jaegertracing/all-in-one:latest +docker run -d -p 8080:80 --link jaeger:jaeger nginx-example-hotrod +``` +Visit http://localhost:8080 to interact with the demo and http://localhost:16686 +to view the traces in Jaeger. diff --git a/example/hotrod/hotrod/cmd/all.go b/example/hotrod/hotrod/cmd/all.go new file mode 100644 index 00000000..5016fd27 --- /dev/null +++ b/example/hotrod/hotrod/cmd/all.go @@ -0,0 +1,35 @@ +// 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.`, + Run: func(cmd *cobra.Command, args []string) { + logger.Info("Starting all services") + go customerCmd.RunE(customerCmd, args) + go driverCmd.RunE(driverCmd, args) + go routeCmd.RunE(routeCmd, args) + frontendCmd.RunE(frontendCmd, args) + }, +} + +func init() { + RootCmd.AddCommand(allCmd) +} diff --git a/example/hotrod/hotrod/cmd/customer.go b/example/hotrod/hotrod/cmd/customer.go new file mode 100644 index 00000000..7ddf6d4f --- /dev/null +++ b/example/hotrod/hotrod/cmd/customer.go @@ -0,0 +1,58 @@ +// 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/rnburn/hotrod-docker/pkg/log" + "github.com/rnburn/hotrod-docker/pkg/tracing" + "github.com/rnburn/hotrod-docker/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 { + logger := log.NewFactory(logger.With(zap.String("service", "customer"))) + server := customer.NewServer( + net.JoinHostPort(customerOptions.serverInterface, strconv.Itoa(customerOptions.serverPort)), + tracing.Init("customer", metricsFactory.Namespace("customer", nil), logger), + metricsFactory, + logger, + ) + return server.Run() + }, +} + +var ( + customerOptions struct { + serverInterface string + serverPort int + } +) + +func init() { + RootCmd.AddCommand(customerCmd) + + customerCmd.Flags().StringVarP(&customerOptions.serverInterface, "bind", "", "127.0.0.1", "interface to which the Customer server will bind") + customerCmd.Flags().IntVarP(&customerOptions.serverPort, "port", "p", 8081, "port on which the Customer server will listen") +} diff --git a/example/hotrod/hotrod/cmd/driver.go b/example/hotrod/hotrod/cmd/driver.go new file mode 100644 index 00000000..0c401095 --- /dev/null +++ b/example/hotrod/hotrod/cmd/driver.go @@ -0,0 +1,58 @@ +// 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/rnburn/hotrod-docker/pkg/log" + "github.com/rnburn/hotrod-docker/pkg/tracing" + "github.com/rnburn/hotrod-docker/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 { + logger := log.NewFactory(logger.With(zap.String("service", "driver"))) + server := driver.NewServer( + net.JoinHostPort(driverOptions.serverInterface, strconv.Itoa(driverOptions.serverPort)), + tracing.Init("driver", metricsFactory.Namespace("driver", nil), logger), + metricsFactory, + logger, + ) + return server.Run() + }, +} + +var ( + driverOptions struct { + serverInterface string + serverPort int + } +) + +func init() { + RootCmd.AddCommand(driverCmd) + + driverCmd.Flags().StringVarP(&driverOptions.serverInterface, "bind", "", "127.0.0.1", "interface to which the driver server will bind") + driverCmd.Flags().IntVarP(&driverOptions.serverPort, "port", "p", 8082, "port on which the driver server will listen") +} diff --git a/example/hotrod/hotrod/cmd/frontend.go b/example/hotrod/hotrod/cmd/frontend.go new file mode 100644 index 00000000..6841edec --- /dev/null +++ b/example/hotrod/hotrod/cmd/frontend.go @@ -0,0 +1,57 @@ +// 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/rnburn/hotrod-docker/pkg/log" + "github.com/rnburn/hotrod-docker/pkg/tracing" + "github.com/rnburn/hotrod-docker/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 { + logger := log.NewFactory(logger.With(zap.String("service", "frontend"))) + server := frontend.NewServer( + net.JoinHostPort(frontendOptions.serverInterface, strconv.Itoa(frontendOptions.serverPort)), + tracing.Init("frontend", metricsFactory.Namespace("frontend", nil), logger), + logger, + ) + return server.Run() + }, +} + +var ( + frontendOptions struct { + serverInterface string + serverPort int + } +) + +func init() { + RootCmd.AddCommand(frontendCmd) + + frontendCmd.Flags().StringVarP(&frontendOptions.serverInterface, "bind", "", "", "interface to which the frontend server will bind") + frontendCmd.Flags().IntVarP(&frontendOptions.serverPort, "port", "p", 8080, "port on which the frontend server will listen") +} diff --git a/example/hotrod/hotrod/cmd/root.go b/example/hotrod/hotrod/cmd/root.go new file mode 100644 index 00000000..0b16e7c9 --- /dev/null +++ b/example/hotrod/hotrod/cmd/root.go @@ -0,0 +1,75 @@ +// 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 ( + "fmt" + "math/rand" + "os" + "time" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/uber/jaeger-lib/metrics" + "github.com/uber/jaeger-lib/metrics/go-kit" + "github.com/uber/jaeger-lib/metrics/go-kit/expvar" + "go.uber.org/zap" +) + +var ( + cfgFile string + logger *zap.Logger + metricsFactory metrics.Factory +) + +// RootCmd represents the base command when called without any subcommands +var RootCmd = &cobra.Command{ + Use: "jaeger-demo", + 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() { + cobra.OnInitialize(initConfig) + logger, _ = zap.NewDevelopment() + metricsFactory = xkit.Wrap("", expvar.NewFactory(10)) // 10 buckets for histograms +} + +// initConfig reads in config file and ENV variables if set. +func initConfig() { + if cfgFile != "" { // enable ability to specify config file via flag + viper.SetConfigFile(cfgFile) + } + + viper.SetConfigName(".jaeger-demo") // name of config file (without extension) + viper.AddConfigPath("$HOME") // adding home directory as first search path + viper.AutomaticEnv() // read in environment variables that match + + // If a config file is found, read it in. + if err := viper.ReadInConfig(); err == nil { + fmt.Println("Using config file:", viper.ConfigFileUsed()) + } + + rand.Seed(int64(time.Now().Nanosecond())) +} diff --git a/example/hotrod/hotrod/cmd/route.go b/example/hotrod/hotrod/cmd/route.go new file mode 100644 index 00000000..c4d5a51c --- /dev/null +++ b/example/hotrod/hotrod/cmd/route.go @@ -0,0 +1,57 @@ +// 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/rnburn/hotrod-docker/pkg/log" + "github.com/rnburn/hotrod-docker/pkg/tracing" + "github.com/rnburn/hotrod-docker/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 { + logger := log.NewFactory(logger.With(zap.String("service", "route"))) + server := route.NewServer( + net.JoinHostPort(routeOptions.serverInterface, strconv.Itoa(routeOptions.serverPort)), + tracing.Init("route", metricsFactory.Namespace("route", nil), logger), + logger, + ) + return server.Run() + }, +} + +var ( + routeOptions struct { + serverInterface string + serverPort int + } +) + +func init() { + RootCmd.AddCommand(routeCmd) + + routeCmd.Flags().StringVarP(&routeOptions.serverInterface, "bind", "", "127.0.0.1", "interface to which the Route server will bind") + routeCmd.Flags().IntVarP(&routeOptions.serverPort, "port", "p", 8083, "port on which the Route server will listen") +} diff --git a/example/hotrod/hotrod/glide.lock b/example/hotrod/hotrod/glide.lock new file mode 100644 index 00000000..b829ce60 --- /dev/null +++ b/example/hotrod/hotrod/glide.lock @@ -0,0 +1,126 @@ +hash: 5afb055333607d4ef6fdd4023a87611599776f05195058dd4bf7a8381dad0bec +updated: 2017-10-20T13:24:51.186338625-07:00 +imports: +- name: github.com/apache/thrift + version: b2a4d4ae21c789b689dd162deb819665567f481c + subpackages: + - lib/go/thrift +- name: github.com/codahale/hdrhistogram + version: 3a0bb77429bd3a61596f5e8a3172445844342120 +- name: github.com/fsnotify/fsnotify + version: 4da3e2cfbabc9f751898f250b49f2439785783a1 +- name: github.com/go-kit/kit + version: a9ca6725cbbea455e61c6bc8a1ed28e81eb3493b + subpackages: + - metrics + - metrics/expvar + - metrics/generic + - metrics/internal/lv +- name: github.com/hashicorp/hcl + version: 23c074d0eceb2b8a5bfdbb271ab780cde70f05a8 + subpackages: + - hcl/ast + - hcl/parser + - hcl/scanner + - hcl/strconv + - hcl/token + - json/parser + - json/scanner + - json/token +- name: github.com/inconshreveable/mousetrap + version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 +- name: github.com/magiconair/properties + version: 8d7837e64d3c1ee4e54a880c5a920ab4316fc90a +- name: github.com/mitchellh/mapstructure + version: 06020f85339e21b2478f756a78e295255ffa4d6a +- name: github.com/opentracing-contrib/go-stdlib + version: 48e4d763b2fbcd10e666e6a1742acdf8cc2286ef + subpackages: + - nethttp +- name: github.com/opentracing/opentracing-go + version: 1949ddbfd147afd4d964a9f00b24eb291e0e7c38 + subpackages: + - ext + - log +- name: github.com/pelletier/go-toml + version: 2009e44b6f182e34d8ce081ac2767622937ea3d4 +- name: github.com/spf13/afero + version: e67d870304c4bca21331b02f414f970df13aa694 + subpackages: + - mem +- name: github.com/spf13/cast + version: acbeb36b902d72a7a4c18e8f3241075e7ab763e4 +- name: github.com/spf13/cobra + version: 7b2c5ac9fc04fc5efafb60700713d4fa609b777b +- name: github.com/spf13/jwalterweatherman + version: 12bd96e66386c1960ab0f74ced1362f66f552f7b +- name: github.com/spf13/pflag + version: 97afa5e7ca8a08a383cb259e06636b5e2cc7897f +- name: github.com/spf13/viper + version: 25b30aa063fc18e48662b86996252eabdcf2f0c7 +- name: github.com/uber-go/atomic + version: 4e336646b2ef9fc6e47be8e21594178f98e5ebcf +- name: github.com/uber/jaeger-client-go + version: 3e3870040def0ebdaf65a003863fa64f5cb26139 + subpackages: + - config + - internal/spanlog + - log + - rpcmetrics + - thrift-gen/agent + - thrift-gen/jaeger + - thrift-gen/sampling + - thrift-gen/zipkincore + - utils +- name: github.com/uber/jaeger-lib + version: 3b2a9ad2a045881ab7a0f81d465be54c8292ee4f + subpackages: + - metrics + - metrics/go-kit + - metrics/go-kit/expvar +- name: github.com/uber/tchannel-go + version: a7ad9ecb640b5f10a0395b38d6319175172b3ab2 + subpackages: + - internal/argreader + - relay + - thrift + - thrift/gen-go/meta + - tnet + - tos + - trand + - typed +- name: github.com/VividCortex/gohistogram + version: 51564d9861991fb0ad0f531c99ef602d0f9866e6 +- name: go.uber.org/atomic + version: 4e336646b2ef9fc6e47be8e21594178f98e5ebcf +- name: go.uber.org/multierr + version: 3c4937480c32f4c13a875a1829af76c98ca3d40a +- name: go.uber.org/zap + version: 35aad584952c3e7020db7b839f6b102de6271f89 + subpackages: + - buffer + - internal/bufferpool + - internal/color + - internal/exit + - zapcore +- name: golang.org/x/net + version: aabf50738bcdd9b207582cbe796b59ed65d56680 + subpackages: + - bpf + - context + - internal/iana + - internal/socket + - ipv4 + - ipv6 +- name: golang.org/x/sys + version: 8dbc5d05d6edcc104950cc299a1ce6641235bc86 + subpackages: + - unix +- name: golang.org/x/text + version: c01e4764d870b77f8abe5096ee19ad20d80e8075 + subpackages: + - transform + - unicode/norm +- name: gopkg.in/yaml.v2 + version: eb3733d160e74a9c7e442f435eb3bea458e1d19f +testImports: [] diff --git a/example/hotrod/hotrod/glide.yaml b/example/hotrod/hotrod/glide.yaml new file mode 100644 index 00000000..519d0f67 --- /dev/null +++ b/example/hotrod/hotrod/glide.yaml @@ -0,0 +1,36 @@ +package: github.com/rnburn/hotrod-docker +import: +- package: github.com/apache/thrift + version: ~0.10.0 + subpackages: + - lib/go/thrift +- package: github.com/opentracing-contrib/go-stdlib + subpackages: + - nethttp +- package: github.com/opentracing/opentracing-go + version: ~1.0.2 + subpackages: + - ext + - log +- package: github.com/spf13/cobra +- package: github.com/spf13/viper + version: ~1.0.0 +- package: github.com/uber/jaeger-client-go + version: ~2.9.0 + subpackages: + - config + - rpcmetrics +- package: github.com/uber/jaeger-lib + version: ~1.1.0 + subpackages: + - metrics + - metrics/go-kit + - metrics/go-kit/expvar +- package: github.com/uber/tchannel-go + version: ~1.7.0 + subpackages: + - thrift +- package: go.uber.org/zap + version: ~1.7.1 + subpackages: + - zapcore diff --git a/example/hotrod/hotrod/main.go b/example/hotrod/hotrod/main.go new file mode 100644 index 00000000..b352952c --- /dev/null +++ b/example/hotrod/hotrod/main.go @@ -0,0 +1,14 @@ +// Copyright (c) 2017 Uber Technologies, Inc. + +package main + +import ( + "runtime" + + "github.com/rnburn/hotrod-docker/cmd" +) + +func main() { + runtime.GOMAXPROCS(runtime.NumCPU()) + cmd.Execute() +} diff --git a/example/hotrod/hotrod/pkg/delay/delay.go b/example/hotrod/hotrod/pkg/delay/delay.go new file mode 100644 index 00000000..d644eed5 --- /dev/null +++ b/example/hotrod/hotrod/pkg/delay/delay.go @@ -0,0 +1,30 @@ +// 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/example/hotrod/hotrod/pkg/httperr/httperr.go b/example/hotrod/hotrod/pkg/httperr/httperr.go new file mode 100644 index 00000000..9351eea7 --- /dev/null +++ b/example/hotrod/hotrod/pkg/httperr/httperr.go @@ -0,0 +1,27 @@ +// 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/example/hotrod/hotrod/pkg/httpexpvar/handler.go b/example/hotrod/hotrod/pkg/httpexpvar/handler.go new file mode 100644 index 00000000..13046eeb --- /dev/null +++ b/example/hotrod/hotrod/pkg/httpexpvar/handler.go @@ -0,0 +1,37 @@ +// 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 httpexpvar + +import ( + "expvar" + "fmt" + "net/http" +) + +// Handler is a copy of expvar.expvarHandler private method. +// TODO this won't be needed in Go 1.8 +func Handler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + fmt.Fprintf(w, "{\n") + first := true + expvar.Do(func(kv expvar.KeyValue) { + if !first { + fmt.Fprintf(w, ",\n") + } + first = false + fmt.Fprintf(w, "%q: %s", kv.Key, kv.Value) + }) + fmt.Fprintf(w, "\n}\n") +} diff --git a/example/hotrod/hotrod/pkg/log/factory.go b/example/hotrod/hotrod/pkg/log/factory.go new file mode 100644 index 00000000..88cc4bb2 --- /dev/null +++ b/example/hotrod/hotrod/pkg/log/factory.go @@ -0,0 +1,55 @@ +// 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" + + "github.com/opentracing/opentracing-go" + "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 logger{logger: b.logger} +} + +// For returns a context-aware Logger. If the context +// contains an OpenTracing span, all logging calls are also +// echo-ed into the span. +func (b Factory) For(ctx context.Context) Logger { + if span := opentracing.SpanFromContext(ctx); span != nil { + // TODO for Jaeger span extract trace/span IDs as fields + return spanLogger{span: span, logger: b.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/example/hotrod/hotrod/pkg/log/logger.go b/example/hotrod/hotrod/pkg/log/logger.go new file mode 100644 index 00000000..a2e1b594 --- /dev/null +++ b/example/hotrod/hotrod/pkg/log/logger.go @@ -0,0 +1,53 @@ +// 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 { + Info(msg string, fields ...zapcore.Field) + Error(msg string, fields ...zapcore.Field) + Fatal(msg string, fields ...zapcore.Field) + With(fields ...zapcore.Field) Logger +} + +// logger delegates all calls to the underlying zap.Logger +type logger struct { + logger *zap.Logger +} + +// Info logs an info msg with fields +func (l logger) Info(msg string, fields ...zapcore.Field) { + l.logger.Info(msg, fields...) +} + +// Error logs an error msg with fields +func (l logger) Error(msg string, fields ...zapcore.Field) { + l.logger.Error(msg, fields...) +} + +// Fatal logs a fatal error msg with fields +func (l logger) 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 logger) With(fields ...zapcore.Field) Logger { + return logger{logger: l.logger.With(fields...)} +} diff --git a/example/hotrod/hotrod/pkg/log/spanlogger.go b/example/hotrod/hotrod/pkg/log/spanlogger.go new file mode 100644 index 00000000..2d0580ac --- /dev/null +++ b/example/hotrod/hotrod/pkg/log/spanlogger.go @@ -0,0 +1,148 @@ +// 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 ( + "time" + + "github.com/opentracing/opentracing-go" + tag "github.com/opentracing/opentracing-go/ext" + "github.com/opentracing/opentracing-go/log" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +type spanLogger struct { + logger *zap.Logger + span opentracing.Span +} + +func (sl spanLogger) Info(msg string, fields ...zapcore.Field) { + sl.logToSpan("info", msg, fields...) + sl.logger.Info(msg, fields...) +} + +func (sl spanLogger) Error(msg string, fields ...zapcore.Field) { + sl.logToSpan("error", msg, fields...) + sl.logger.Error(msg, fields...) +} + +func (sl spanLogger) Fatal(msg string, fields ...zapcore.Field) { + sl.logToSpan("fatal", msg, fields...) + tag.Error.Set(sl.span, true) + sl.logger.Fatal(msg, 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} +} + +func (sl spanLogger) logToSpan(level string, msg string, fields ...zapcore.Field) { + // TODO rather than always converting the fields, we could wrap them into a lazy logger + fa := fieldAdapter(make([]log.Field, 0, 2+len(fields))) + fa = append(fa, log.String("event", msg)) + fa = append(fa, log.String("level", level)) + for _, field := range fields { + field.AddTo(&fa) + } + sl.span.LogFields(fa...) +} + +type fieldAdapter []log.Field + +func (fa *fieldAdapter) AddBool(key string, value bool) { + *fa = append(*fa, log.Bool(key, value)) +} + +func (fa *fieldAdapter) AddFloat64(key string, value float64) { + *fa = append(*fa, log.Float64(key, value)) +} + +func (fa *fieldAdapter) AddFloat32(key string, value float32) { + *fa = append(*fa, log.Float64(key, float64(value))) +} + +func (fa *fieldAdapter) AddInt(key string, value int) { + *fa = append(*fa, log.Int(key, value)) +} + +func (fa *fieldAdapter) AddInt64(key string, value int64) { + *fa = append(*fa, log.Int64(key, value)) +} + +func (fa *fieldAdapter) AddInt32(key string, value int32) { + *fa = append(*fa, log.Int64(key, int64(value))) +} + +func (fa *fieldAdapter) AddInt16(key string, value int16) { + *fa = append(*fa, log.Int64(key, int64(value))) +} + +func (fa *fieldAdapter) AddInt8(key string, value int8) { + *fa = append(*fa, log.Int64(key, int64(value))) +} + +func (fa *fieldAdapter) AddUint(key string, value uint) { + *fa = append(*fa, log.Uint64(key, uint64(value))) +} + +func (fa *fieldAdapter) AddUint64(key string, value uint64) { + *fa = append(*fa, log.Uint64(key, value)) +} + +func (fa *fieldAdapter) AddUint32(key string, value uint32) { + *fa = append(*fa, log.Uint64(key, uint64(value))) +} + +func (fa *fieldAdapter) AddUint16(key string, value uint16) { + *fa = append(*fa, log.Uint64(key, uint64(value))) +} + +func (fa *fieldAdapter) AddUint8(key string, value uint8) { + *fa = append(*fa, log.Uint64(key, uint64(value))) +} + +func (fa *fieldAdapter) AddUintptr(key string, value uintptr) {} +func (fa *fieldAdapter) AddArray(key string, marshaler zapcore.ArrayMarshaler) error { return nil } +func (fa *fieldAdapter) AddComplex128(key string, value complex128) {} +func (fa *fieldAdapter) AddComplex64(key string, value complex64) {} +func (fa *fieldAdapter) AddObject(key string, value zapcore.ObjectMarshaler) error { return nil } +func (fa *fieldAdapter) AddReflected(key string, value interface{}) error { return nil } +func (fa *fieldAdapter) OpenNamespace(key string) {} + +func (fa *fieldAdapter) AddDuration(key string, value time.Duration) { + // TODO inefficient + *fa = append(*fa, log.String(key, value.String())) +} + +func (fa *fieldAdapter) AddTime(key string, value time.Time) { + // TODO inefficient + *fa = append(*fa, log.String(key, value.String())) +} + +func (fa *fieldAdapter) AddBinary(key string, value []byte) { + *fa = append(*fa, log.Object(key, value)) +} + +func (fa *fieldAdapter) AddByteString(key string, value []byte) { + *fa = append(*fa, log.Object(key, value)) +} + +func (fa *fieldAdapter) AddString(key, value string) { + if key != "" && value != "" { + *fa = append(*fa, log.String(key, value)) + } +} diff --git a/example/hotrod/hotrod/pkg/pool/pool.go b/example/hotrod/hotrod/pkg/pool/pool.go new file mode 100644 index 00000000..172064e7 --- /dev/null +++ b/example/hotrod/hotrod/pkg/pool/pool.go @@ -0,0 +1,53 @@ +// 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() { + p.stop <- struct{}{} +} diff --git a/example/hotrod/hotrod/pkg/tracing/http.go b/example/hotrod/hotrod/pkg/tracing/http.go new file mode 100644 index 00000000..6707602b --- /dev/null +++ b/example/hotrod/hotrod/pkg/tracing/http.go @@ -0,0 +1,61 @@ +// 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/ioutil" + "net/http" + + "github.com/opentracing-contrib/go-stdlib/nethttp" + "github.com/opentracing/opentracing-go" +) + +// HTTPClient wraps an http.Client with tracing instrumentation. +type HTTPClient struct { + Tracer opentracing.Tracer + Client *http.Client +} + +// 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.NewRequest("GET", url, nil) + if err != nil { + return err + } + req = req.WithContext(ctx) + req, ht := nethttp.TraceRequest(c.Tracer, req, nethttp.OperationName("HTTP GET: "+endpoint)) + defer ht.Finish() + + res, err := c.Client.Do(req) + if err != nil { + return err + } + + defer res.Body.Close() + + if res.StatusCode >= 400 { + body, err := ioutil.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/example/hotrod/hotrod/pkg/tracing/init.go b/example/hotrod/hotrod/pkg/tracing/init.go new file mode 100644 index 00000000..482b320e --- /dev/null +++ b/example/hotrod/hotrod/pkg/tracing/init.go @@ -0,0 +1,73 @@ +// 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 ( + "fmt" + "time" + "os" + + "github.com/opentracing/opentracing-go" + "github.com/uber/jaeger-client-go/config" + "github.com/uber/jaeger-client-go/rpcmetrics" + "github.com/uber/jaeger-lib/metrics" + "go.uber.org/zap" + + "github.com/rnburn/hotrod-docker/pkg/log" +) + +// Init creates a new instance of Jaeger tracer. +func Init(serviceName string, metricsFactory metrics.Factory, logger log.Factory) opentracing.Tracer { + localAgentHost := os.Getenv("JAEGER_AGENT_HOST") + if localAgentHost == "" { + localAgentHost = "localhost" + } + localAgentPort := os.Getenv("JAEGER_AGENT_PORT") + if localAgentPort == "" { + localAgentPort = "6831" + } + cfg := config.Configuration{ + Sampler: &config.SamplerConfig{ + Type: "const", + Param: 1, + }, + Reporter: &config.ReporterConfig{ + LogSpans: false, + BufferFlushInterval: 1 * time.Second, + LocalAgentHostPort: fmt.Sprintf("%s:%s", localAgentHost, localAgentPort), + }, + } + tracer, _, err := cfg.New( + serviceName, + config.Logger(jaegerLoggerAdapter{logger.Bg()}), + config.Observer(rpcmetrics.NewObserver(metricsFactory, rpcmetrics.DefaultNameNormalizer)), + ) + if err != nil { + logger.Bg().Fatal("cannot initialize Jaeger Tracer", zap.Error(err)) + } + return tracer +} + +type jaegerLoggerAdapter struct { + logger log.Logger +} + +func (l jaegerLoggerAdapter) Error(msg string) { + l.logger.Error(msg) +} + +func (l jaegerLoggerAdapter) Infof(msg string, args ...interface{}) { + l.logger.Info(fmt.Sprintf(msg, args...)) +} diff --git a/example/hotrod/hotrod/pkg/tracing/mutex.go b/example/hotrod/hotrod/pkg/tracing/mutex.go new file mode 100644 index 00000000..b7da3fdc --- /dev/null +++ b/example/hotrod/hotrod/pkg/tracing/mutex.go @@ -0,0 +1,82 @@ +// 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" + + "github.com/opentracing/opentracing-go" + "github.com/opentracing/opentracing-go/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 + + realLock sync.Mutex + holder string + + waiters []string + waitersLock sync.Mutex +} + +// Lock acquires an exclusive lock. +func (sm *Mutex) Lock(ctx context.Context) { + var session string + activeSpan := opentracing.SpanFromContext(ctx) + if activeSpan != nil { + session = activeSpan.BaggageItem(sm.SessionBaggageKey) + activeSpan.SetTag(sm.SessionBaggageKey, session) + } + + sm.waitersLock.Lock() + if waiting := len(sm.waiters); waiting > 0 && activeSpan != nil { + activeSpan.LogFields( + log.String("event", fmt.Sprintf("Waiting for lock behind %d transactions", waiting)), + log.String("blockers", fmt.Sprintf("%v", sm.waiters))) // avoid deferred slice.String() + fmt.Printf("%s Waiting for lock behind %d transactions: %v\n", session, waiting, 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 + sm.waitersLock.Unlock() + + if activeSpan != nil { + activeSpan.LogEvent( + fmt.Sprintf("Acquired lock with %d transactions waiting behind", behindLen)) + } +} + +// 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/example/hotrod/hotrod/pkg/tracing/mux.go b/example/hotrod/hotrod/pkg/tracing/mux.go new file mode 100644 index 00000000..ac309f0d --- /dev/null +++ b/example/hotrod/hotrod/pkg/tracing/mux.go @@ -0,0 +1,52 @@ +// 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" + + "github.com/opentracing-contrib/go-stdlib/nethttp" + "github.com/opentracing/opentracing-go" +) + +// NewServeMux creates a new TracedServeMux. +func NewServeMux(tracer opentracing.Tracer) *TracedServeMux { + return &TracedServeMux{ + mux: http.NewServeMux(), + tracer: tracer, + } +} + +// TracedServeMux is a wrapper around http.ServeMux that instruments handlers for tracing. +type TracedServeMux struct { + mux *http.ServeMux + tracer opentracing.Tracer +} + +// Handle implements http.ServeMux#Handle +func (tm *TracedServeMux) Handle(pattern string, handler http.Handler) { + middleware := nethttp.Middleware( + tm.tracer, + handler, + nethttp.OperationNameFunc(func(r *http.Request) string { + return "HTTP " + r.Method + " " + pattern + })) + 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) +} diff --git a/example/hotrod/hotrod/services/config/config.go b/example/hotrod/hotrod/services/config/config.go new file mode 100644 index 00000000..28cccd74 --- /dev/null +++ b/example/hotrod/hotrod/services/config/config.go @@ -0,0 +1,60 @@ +// 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 + + // WorkerPoolSize is the size of goroutine pool used to query for routes + WorkerPoolSize = 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 + + // RouteWorkerPoolSize is the size of the worker pool used to query `route` service + RouteWorkerPoolSize = 3 + + // '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/example/hotrod/hotrod/services/customer/client.go b/example/hotrod/hotrod/services/customer/client.go new file mode 100644 index 00000000..95316f33 --- /dev/null +++ b/example/hotrod/hotrod/services/customer/client.go @@ -0,0 +1,59 @@ +// 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" + "net/http" + + "github.com/opentracing-contrib/go-stdlib/nethttp" + "github.com/opentracing/opentracing-go" + "go.uber.org/zap" + + "github.com/rnburn/hotrod-docker/pkg/log" + "github.com/rnburn/hotrod-docker/pkg/tracing" +) + +// Client is a remote client that implements customer.Interface +type Client struct { + tracer opentracing.Tracer + logger log.Factory + client *tracing.HTTPClient +} + +// NewClient creates a new customer.Client +func NewClient(tracer opentracing.Tracer, logger log.Factory) *Client { + return &Client{ + tracer: tracer, + logger: logger, + client: &tracing.HTTPClient{ + Client: &http.Client{Transport: &nethttp.Transport{}}, + Tracer: tracer, + }, + } +} + +// Get implements customer.Interface#Get as an RPC +func (c *Client) Get(ctx context.Context, customerID string) (*Customer, error) { + c.logger.For(ctx).Info("Getting customer", zap.String("customer_id", customerID)) + + url := fmt.Sprintf("http://127.0.0.1/customer?customer=%s", customerID) + var customer Customer + if err := c.client.GetJSON(ctx, "/customer", url, &customer); err != nil { + return nil, err + } + return &customer, nil +} diff --git a/example/hotrod/hotrod/services/customer/database.go b/example/hotrod/hotrod/services/customer/database.go new file mode 100644 index 00000000..41078bbf --- /dev/null +++ b/example/hotrod/hotrod/services/customer/database.go @@ -0,0 +1,96 @@ +// 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" + + "go.uber.org/zap" + + "github.com/opentracing/opentracing-go" + tags "github.com/opentracing/opentracing-go/ext" + + "github.com/rnburn/hotrod-docker/pkg/delay" + "github.com/rnburn/hotrod-docker/pkg/log" + "github.com/rnburn/hotrod-docker/pkg/tracing" + "github.com/rnburn/hotrod-docker/services/config" +) + +// database simulates Customer repository implemented on top of an SQL database +type database struct { + tracer opentracing.Tracer + logger log.Factory + customers map[string]*Customer + lock *tracing.Mutex +} + +func newDatabase(tracer opentracing.Tracer, logger log.Factory) *database { + return &database{ + tracer: tracer, + logger: logger, + lock: &tracing.Mutex{ + SessionBaggageKey: "request", + }, + customers: map[string]*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 Deserts", + Location: "728,326", + }, + }, + } +} + +func (d *database) Get(ctx context.Context, customerID string) (*Customer, error) { + d.logger.For(ctx).Info("Loading customer", zap.String("customer_id", customerID)) + + // simulate opentracing instrumentation of an SQL query + if span := opentracing.SpanFromContext(ctx); span != nil { + span := d.tracer.StartSpan("SQL SELECT", opentracing.ChildOf(span.Context())) + tags.SpanKindRPCClient.Set(span) + tags.PeerService.Set(span, "mysql") + span.SetTag("sql.query", "SELECT * FROM customer WHERE customer_id="+customerID) + defer span.Finish() + ctx = opentracing.ContextWithSpan(ctx, span) + } + + // 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/example/hotrod/hotrod/services/customer/interface.go b/example/hotrod/hotrod/services/customer/interface.go new file mode 100644 index 00000000..976adc9a --- /dev/null +++ b/example/hotrod/hotrod/services/customer/interface.go @@ -0,0 +1,29 @@ +// 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 string) (*Customer, error) +} diff --git a/example/hotrod/hotrod/services/customer/server.go b/example/hotrod/hotrod/services/customer/server.go new file mode 100644 index 00000000..c3f44a2a --- /dev/null +++ b/example/hotrod/hotrod/services/customer/server.go @@ -0,0 +1,92 @@ +// 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" + + "github.com/opentracing/opentracing-go" + "github.com/uber/jaeger-lib/metrics" + "go.uber.org/zap" + + "github.com/rnburn/hotrod-docker/pkg/httperr" + "github.com/rnburn/hotrod-docker/pkg/log" + "github.com/rnburn/hotrod-docker/pkg/tracing" +) + +// Server implements Customer service +type Server struct { + hostPort string + tracer opentracing.Tracer + logger log.Factory + database *database +} + +// NewServer creates a new customer.Server +func NewServer(hostPort string, tracer opentracing.Tracer, metricsFactory metrics.Factory, logger log.Factory) *Server { + return &Server{ + hostPort: hostPort, + tracer: tracer, + logger: logger, + database: newDatabase( + tracing.Init("mysql", metricsFactory.Namespace("mysql", nil), logger), + 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)) + return http.ListenAndServe(s.hostPort, mux) +} + +func (s *Server) createServeMux() http.Handler { + mux := tracing.NewServeMux(s.tracer) + 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 + } + + customerID := r.Form.Get("customer") + if customerID == "" { + http.Error(w, "Missing required 'customer' parameter", 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/example/hotrod/hotrod/services/driver/client.go b/example/hotrod/hotrod/services/driver/client.go new file mode 100644 index 00000000..9a8d3f75 --- /dev/null +++ b/example/hotrod/hotrod/services/driver/client.go @@ -0,0 +1,84 @@ +// 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" + + "github.com/opentracing/opentracing-go" + "github.com/uber/tchannel-go" + "github.com/uber/tchannel-go/thrift" + "go.uber.org/zap" + + "github.com/rnburn/hotrod-docker/pkg/log" + "github.com/rnburn/hotrod-docker/services/driver/thrift-gen/driver" +) + +// Client is a remote client that implements driver.Interface +type Client struct { + tracer opentracing.Tracer + logger log.Factory + ch *tchannel.Channel + client driver.TChanDriver +} + +// NewClient creates a new driver.Client +func NewClient(tracer opentracing.Tracer, logger log.Factory) *Client { + channelOpts := &tchannel.ChannelOptions{ + //Logger: logger, + //StatsReporter: statsReporter, + Tracer: tracer, + } + ch, err := tchannel.NewChannel("driver-client", channelOpts) + if err != nil { + logger.Bg().Fatal("Cannot create TChannel", zap.Error(err)) + } + clientOpts := &thrift.ClientOptions{ + HostPort: "127.0.0.1:8082", + } + thriftClient := thrift.NewClient(ch, "driver", clientOpts) + client := driver.NewTChanDriverClient(thriftClient) + + return &Client{ + tracer: tracer, + logger: logger, + ch: ch, + 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() + results, err := c.client.FindNearest(thrift.Wrap(ctx), location) + if err != nil { + return nil, err + } + return fromThrift(results), nil +} + +func fromThrift(results []*driver.DriverLocation) []Driver { + retMe := make([]Driver, len(results)) + for i, result := range results { + retMe[i] = Driver{ + DriverID: result.DriverID, + Location: result.Location, + } + } + return retMe +} diff --git a/example/hotrod/hotrod/services/driver/driver.thrift b/example/hotrod/hotrod/services/driver/driver.thrift new file mode 100644 index 00000000..0b6778f3 --- /dev/null +++ b/example/hotrod/hotrod/services/driver/driver.thrift @@ -0,0 +1,9 @@ + +struct DriverLocation { + 1: required string driver_id + 2: required string location +} + +service Driver { + list findNearest(1: string location) +} \ No newline at end of file diff --git a/example/hotrod/hotrod/services/driver/interface.go b/example/hotrod/hotrod/services/driver/interface.go new file mode 100644 index 00000000..d20941f4 --- /dev/null +++ b/example/hotrod/hotrod/services/driver/interface.go @@ -0,0 +1,28 @@ +// 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 currentl 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/example/hotrod/hotrod/services/driver/redis.go b/example/hotrod/hotrod/services/driver/redis.go new file mode 100644 index 00000000..3e1edf6f --- /dev/null +++ b/example/hotrod/hotrod/services/driver/redis.go @@ -0,0 +1,111 @@ +// 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" + + "github.com/opentracing/opentracing-go" + "github.com/opentracing/opentracing-go/ext" + "github.com/uber/jaeger-lib/metrics" + "go.uber.org/zap" + + "github.com/rnburn/hotrod-docker/pkg/delay" + "github.com/rnburn/hotrod-docker/pkg/log" + "github.com/rnburn/hotrod-docker/pkg/tracing" + "github.com/rnburn/hotrod-docker/services/config" +) + +// Redis is a simulator of remote Redis cache +type Redis struct { + tracer opentracing.Tracer // simulate redis as a separate process + logger log.Factory + errorSimulator +} + +func newRedis(metricsFactory metrics.Factory, logger log.Factory) *Redis { + return &Redis{ + tracer: tracing.Init("redis", metricsFactory.Namespace("redis", nil), logger), + logger: logger, + } +} + +// FindDriverIDs finds IDs of drivers who are near the location. +func (r *Redis) FindDriverIDs(ctx context.Context, location string) []string { + if span := opentracing.SpanFromContext(ctx); span != nil { + span := r.tracer.StartSpan("FindDriverIDs", opentracing.ChildOf(span.Context())) + span.SetTag("param.location", location) + ext.SpanKindRPCClient.Set(span) + defer span.Finish() + ctx = opentracing.ContextWithSpan(ctx, span) + } + // simulate RPC delay + delay.Sleep(config.RedisFindDelay, config.RedisFindDelayStdDev) + + drivers := make([]string, 10) + for i := range drivers { + drivers[i] = fmt.Sprintf("T7%05dC", rand.Int()%100000) + } + return drivers +} + +// GetDriver returns driver and the current car location +func (r *Redis) GetDriver(ctx context.Context, driverID string) (Driver, error) { + if span := opentracing.SpanFromContext(ctx); span != nil { + span := r.tracer.StartSpan("GetDriver", opentracing.ChildOf(span.Context())) + span.SetTag("param.driverID", driverID) + ext.SpanKindRPCClient.Set(span) + defer span.Finish() + ctx = opentracing.ContextWithSpan(ctx, span) + } + // simulate RPC delay + delay.Sleep(config.RedisGetDelay, config.RedisGetDelayStdDev) + if err := r.checkError(); err != nil { + if span := opentracing.SpanFromContext(ctx); span != nil { + ext.Error.Set(span, true) + } + r.logger.For(ctx).Error("redis timeout", zap.String("driver_id", driverID), zap.Error(err)) + return Driver{}, err + } + + 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/example/hotrod/hotrod/services/driver/server.go b/example/hotrod/hotrod/services/driver/server.go new file mode 100644 index 00000000..a514cac2 --- /dev/null +++ b/example/hotrod/hotrod/services/driver/server.go @@ -0,0 +1,102 @@ +// 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 ( + "github.com/opentracing/opentracing-go" + "github.com/uber/jaeger-lib/metrics" + "github.com/uber/tchannel-go" + "github.com/uber/tchannel-go/thrift" + "go.uber.org/zap" + + "github.com/rnburn/hotrod-docker/pkg/log" + "github.com/rnburn/hotrod-docker/services/driver/thrift-gen/driver" +) + +// Server implements jaeger-demo-frontend service +type Server struct { + hostPort string + tracer opentracing.Tracer + logger log.Factory + ch *tchannel.Channel + server *thrift.Server + redis *Redis +} + +// NewServer creates a new driver.Server +func NewServer(hostPort string, tracer opentracing.Tracer, metricsFactory metrics.Factory, logger log.Factory) *Server { + channelOpts := &tchannel.ChannelOptions{ + Tracer: tracer, + } + ch, err := tchannel.NewChannel("driver", channelOpts) + if err != nil { + logger.Bg().Fatal("Cannot create TChannel", zap.Error(err)) + } + server := thrift.NewServer(ch) + + return &Server{ + hostPort: hostPort, + tracer: tracer, + logger: logger, + ch: ch, + server: server, + redis: newRedis(metricsFactory, logger), + } +} + +// Run starts the Driver server +func (s *Server) Run() error { + + s.server.Register(driver.NewTChanDriverServer(s)) + + if err := s.ch.ListenAndServe(s.hostPort); err != nil { + s.logger.Bg().Fatal("Unable to start tchannel server", zap.Error(err)) + } + + peerInfo := s.ch.PeerInfo() + s.logger.Bg().Info("TChannel listening", zap.String("hostPort", peerInfo.HostPort)) + + // Run must block, but TChannel's ListenAndServe runs in the background, so block indefinitely + select {} +} + +// FindNearest implements Thrift interface TChanDriver +func (s *Server) FindNearest(ctx thrift.Context, location string) ([]*driver.DriverLocation, error) { + s.logger.For(ctx).Info("Searching for nearby drivers", zap.String("location", location)) + driverIDs := s.redis.FindDriverIDs(ctx, location) + + retMe := make([]*driver.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 + } + retMe[i] = &driver.DriverLocation{ + DriverID: drv.DriverID, + Location: drv.Location, + } + } + s.logger.For(ctx).Info("Search successful", zap.Int("num_drivers", len(retMe))) + return retMe, nil +} diff --git a/example/hotrod/hotrod/services/driver/thrift-gen/driver/constants.go b/example/hotrod/hotrod/services/driver/thrift-gen/driver/constants.go new file mode 100644 index 00000000..09d65670 --- /dev/null +++ b/example/hotrod/hotrod/services/driver/thrift-gen/driver/constants.go @@ -0,0 +1,18 @@ +// Autogenerated by Thrift Compiler (0.9.3) +// DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING + +package driver + +import ( + "bytes" + "fmt" + "github.com/apache/thrift/lib/go/thrift" +) + +// (needed to ensure safety because of naive import list construction.) +var _ = thrift.ZERO +var _ = fmt.Printf +var _ = bytes.Equal + +func init() { +} diff --git a/example/hotrod/hotrod/services/driver/thrift-gen/driver/driver.go b/example/hotrod/hotrod/services/driver/thrift-gen/driver/driver.go new file mode 100644 index 00000000..c8b20275 --- /dev/null +++ b/example/hotrod/hotrod/services/driver/thrift-gen/driver/driver.go @@ -0,0 +1,427 @@ +// Autogenerated by Thrift Compiler (0.9.3) +// DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING + +package driver + +import ( + "bytes" + "fmt" + "github.com/apache/thrift/lib/go/thrift" +) + +// (needed to ensure safety because of naive import list construction.) +var _ = thrift.ZERO +var _ = fmt.Printf +var _ = bytes.Equal + +type Driver interface { + // Parameters: + // - Location + FindNearest(location string) (r []*DriverLocation, err error) +} + +type DriverClient struct { + Transport thrift.TTransport + ProtocolFactory thrift.TProtocolFactory + InputProtocol thrift.TProtocol + OutputProtocol thrift.TProtocol + SeqId int32 +} + +func NewDriverClientFactory(t thrift.TTransport, f thrift.TProtocolFactory) *DriverClient { + return &DriverClient{Transport: t, + ProtocolFactory: f, + InputProtocol: f.GetProtocol(t), + OutputProtocol: f.GetProtocol(t), + SeqId: 0, + } +} + +func NewDriverClientProtocol(t thrift.TTransport, iprot thrift.TProtocol, oprot thrift.TProtocol) *DriverClient { + return &DriverClient{Transport: t, + ProtocolFactory: nil, + InputProtocol: iprot, + OutputProtocol: oprot, + SeqId: 0, + } +} + +// Parameters: +// - Location +func (p *DriverClient) FindNearest(location string) (r []*DriverLocation, err error) { + if err = p.sendFindNearest(location); err != nil { + return + } + return p.recvFindNearest() +} + +func (p *DriverClient) sendFindNearest(location string) (err error) { + oprot := p.OutputProtocol + if oprot == nil { + oprot = p.ProtocolFactory.GetProtocol(p.Transport) + p.OutputProtocol = oprot + } + p.SeqId++ + if err = oprot.WriteMessageBegin("findNearest", thrift.CALL, p.SeqId); err != nil { + return + } + args := DriverFindNearestArgs{ + Location: location, + } + if err = args.Write(oprot); err != nil { + return + } + if err = oprot.WriteMessageEnd(); err != nil { + return + } + return oprot.Flush() +} + +func (p *DriverClient) recvFindNearest() (value []*DriverLocation, err error) { + iprot := p.InputProtocol + if iprot == nil { + iprot = p.ProtocolFactory.GetProtocol(p.Transport) + p.InputProtocol = iprot + } + method, mTypeId, seqId, err := iprot.ReadMessageBegin() + if err != nil { + return + } + if method != "findNearest" { + err = thrift.NewTApplicationException(thrift.WRONG_METHOD_NAME, "findNearest failed: wrong method name") + return + } + if p.SeqId != seqId { + err = thrift.NewTApplicationException(thrift.BAD_SEQUENCE_ID, "findNearest failed: out of sequence response") + return + } + if mTypeId == thrift.EXCEPTION { + error0 := thrift.NewTApplicationException(thrift.UNKNOWN_APPLICATION_EXCEPTION, "Unknown Exception") + var error1 error + error1, err = error0.Read(iprot) + if err != nil { + return + } + if err = iprot.ReadMessageEnd(); err != nil { + return + } + err = error1 + return + } + if mTypeId != thrift.REPLY { + err = thrift.NewTApplicationException(thrift.INVALID_MESSAGE_TYPE_EXCEPTION, "findNearest failed: invalid message type") + return + } + result := DriverFindNearestResult{} + if err = result.Read(iprot); err != nil { + return + } + if err = iprot.ReadMessageEnd(); err != nil { + return + } + value = result.GetSuccess() + return +} + +type DriverProcessor struct { + processorMap map[string]thrift.TProcessorFunction + handler Driver +} + +func (p *DriverProcessor) AddToProcessorMap(key string, processor thrift.TProcessorFunction) { + p.processorMap[key] = processor +} + +func (p *DriverProcessor) GetProcessorFunction(key string) (processor thrift.TProcessorFunction, ok bool) { + processor, ok = p.processorMap[key] + return processor, ok +} + +func (p *DriverProcessor) ProcessorMap() map[string]thrift.TProcessorFunction { + return p.processorMap +} + +func NewDriverProcessor(handler Driver) *DriverProcessor { + + self2 := &DriverProcessor{handler: handler, processorMap: make(map[string]thrift.TProcessorFunction)} + self2.processorMap["findNearest"] = &driverProcessorFindNearest{handler: handler} + return self2 +} + +func (p *DriverProcessor) Process(iprot, oprot thrift.TProtocol) (success bool, err thrift.TException) { + name, _, seqId, err := iprot.ReadMessageBegin() + if err != nil { + return false, err + } + if processor, ok := p.GetProcessorFunction(name); ok { + return processor.Process(seqId, iprot, oprot) + } + iprot.Skip(thrift.STRUCT) + iprot.ReadMessageEnd() + x3 := thrift.NewTApplicationException(thrift.UNKNOWN_METHOD, "Unknown function "+name) + oprot.WriteMessageBegin(name, thrift.EXCEPTION, seqId) + x3.Write(oprot) + oprot.WriteMessageEnd() + oprot.Flush() + return false, x3 + +} + +type driverProcessorFindNearest struct { + handler Driver +} + +func (p *driverProcessorFindNearest) Process(seqId int32, iprot, oprot thrift.TProtocol) (success bool, err thrift.TException) { + args := DriverFindNearestArgs{} + if err = args.Read(iprot); err != nil { + iprot.ReadMessageEnd() + x := thrift.NewTApplicationException(thrift.PROTOCOL_ERROR, err.Error()) + oprot.WriteMessageBegin("findNearest", thrift.EXCEPTION, seqId) + x.Write(oprot) + oprot.WriteMessageEnd() + oprot.Flush() + return false, err + } + + iprot.ReadMessageEnd() + result := DriverFindNearestResult{} + var retval []*DriverLocation + var err2 error + if retval, err2 = p.handler.FindNearest(args.Location); err2 != nil { + x := thrift.NewTApplicationException(thrift.INTERNAL_ERROR, "Internal error processing findNearest: "+err2.Error()) + oprot.WriteMessageBegin("findNearest", thrift.EXCEPTION, seqId) + x.Write(oprot) + oprot.WriteMessageEnd() + oprot.Flush() + return true, err2 + } else { + result.Success = retval + } + if err2 = oprot.WriteMessageBegin("findNearest", thrift.REPLY, seqId); err2 != nil { + err = err2 + } + if err2 = result.Write(oprot); err == nil && err2 != nil { + err = err2 + } + if err2 = oprot.WriteMessageEnd(); err == nil && err2 != nil { + err = err2 + } + if err2 = oprot.Flush(); err == nil && err2 != nil { + err = err2 + } + if err != nil { + return + } + return true, err +} + +// HELPER FUNCTIONS AND STRUCTURES + +// Attributes: +// - Location +type DriverFindNearestArgs struct { + Location string `thrift:"location,1" json:"location"` +} + +func NewDriverFindNearestArgs() *DriverFindNearestArgs { + return &DriverFindNearestArgs{} +} + +func (p *DriverFindNearestArgs) GetLocation() string { + return p.Location +} +func (p *DriverFindNearestArgs) Read(iprot thrift.TProtocol) error { + if _, err := iprot.ReadStructBegin(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T read error: ", p), err) + } + + for { + _, fieldTypeId, fieldId, err := iprot.ReadFieldBegin() + if err != nil { + return thrift.PrependError(fmt.Sprintf("%T field %d read error: ", p, fieldId), err) + } + if fieldTypeId == thrift.STOP { + break + } + switch fieldId { + case 1: + if err := p.readField1(iprot); err != nil { + return err + } + default: + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + if err := iprot.ReadFieldEnd(); err != nil { + return err + } + } + if err := iprot.ReadStructEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err) + } + return nil +} + +func (p *DriverFindNearestArgs) readField1(iprot thrift.TProtocol) error { + if v, err := iprot.ReadString(); err != nil { + return thrift.PrependError("error reading field 1: ", err) + } else { + p.Location = v + } + return nil +} + +func (p *DriverFindNearestArgs) Write(oprot thrift.TProtocol) error { + if err := oprot.WriteStructBegin("findNearest_args"); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) + } + if err := p.writeField1(oprot); err != nil { + return err + } + if err := oprot.WriteFieldStop(); err != nil { + return thrift.PrependError("write field stop error: ", err) + } + if err := oprot.WriteStructEnd(); err != nil { + return thrift.PrependError("write struct stop error: ", err) + } + return nil +} + +func (p *DriverFindNearestArgs) writeField1(oprot thrift.TProtocol) (err error) { + if err := oprot.WriteFieldBegin("location", thrift.STRING, 1); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field begin error 1:location: ", p), err) + } + if err := oprot.WriteString(string(p.Location)); err != nil { + return thrift.PrependError(fmt.Sprintf("%T.location (1) field write error: ", p), err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field end error 1:location: ", p), err) + } + return err +} + +func (p *DriverFindNearestArgs) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("DriverFindNearestArgs(%+v)", *p) +} + +// Attributes: +// - Success +type DriverFindNearestResult struct { + Success []*DriverLocation `thrift:"success,0" json:"success,omitempty"` +} + +func NewDriverFindNearestResult() *DriverFindNearestResult { + return &DriverFindNearestResult{} +} + +var DriverFindNearestResult_Success_DEFAULT []*DriverLocation + +func (p *DriverFindNearestResult) GetSuccess() []*DriverLocation { + return p.Success +} +func (p *DriverFindNearestResult) IsSetSuccess() bool { + return p.Success != nil +} + +func (p *DriverFindNearestResult) Read(iprot thrift.TProtocol) error { + if _, err := iprot.ReadStructBegin(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T read error: ", p), err) + } + + for { + _, fieldTypeId, fieldId, err := iprot.ReadFieldBegin() + if err != nil { + return thrift.PrependError(fmt.Sprintf("%T field %d read error: ", p, fieldId), err) + } + if fieldTypeId == thrift.STOP { + break + } + switch fieldId { + case 0: + if err := p.readField0(iprot); err != nil { + return err + } + default: + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + if err := iprot.ReadFieldEnd(); err != nil { + return err + } + } + if err := iprot.ReadStructEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err) + } + return nil +} + +func (p *DriverFindNearestResult) readField0(iprot thrift.TProtocol) error { + _, size, err := iprot.ReadListBegin() + if err != nil { + return thrift.PrependError("error reading list begin: ", err) + } + tSlice := make([]*DriverLocation, 0, size) + p.Success = tSlice + for i := 0; i < size; i++ { + _elem4 := &DriverLocation{} + if err := _elem4.Read(iprot); err != nil { + return thrift.PrependError(fmt.Sprintf("%T error reading struct: ", _elem4), err) + } + p.Success = append(p.Success, _elem4) + } + if err := iprot.ReadListEnd(); err != nil { + return thrift.PrependError("error reading list end: ", err) + } + return nil +} + +func (p *DriverFindNearestResult) Write(oprot thrift.TProtocol) error { + if err := oprot.WriteStructBegin("findNearest_result"); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) + } + if err := p.writeField0(oprot); err != nil { + return err + } + if err := oprot.WriteFieldStop(); err != nil { + return thrift.PrependError("write field stop error: ", err) + } + if err := oprot.WriteStructEnd(); err != nil { + return thrift.PrependError("write struct stop error: ", err) + } + return nil +} + +func (p *DriverFindNearestResult) writeField0(oprot thrift.TProtocol) (err error) { + if p.IsSetSuccess() { + if err := oprot.WriteFieldBegin("success", thrift.LIST, 0); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field begin error 0:success: ", p), err) + } + if err := oprot.WriteListBegin(thrift.STRUCT, len(p.Success)); err != nil { + return thrift.PrependError("error writing list begin: ", err) + } + for _, v := range p.Success { + if err := v.Write(oprot); err != nil { + return thrift.PrependError(fmt.Sprintf("%T error writing struct: ", v), err) + } + } + if err := oprot.WriteListEnd(); err != nil { + return thrift.PrependError("error writing list end: ", err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field end error 0:success: ", p), err) + } + } + return err +} + +func (p *DriverFindNearestResult) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("DriverFindNearestResult(%+v)", *p) +} diff --git a/example/hotrod/hotrod/services/driver/thrift-gen/driver/tchan-driver.go b/example/hotrod/hotrod/services/driver/thrift-gen/driver/tchan-driver.go new file mode 100644 index 00000000..d69dfd23 --- /dev/null +++ b/example/hotrod/hotrod/services/driver/thrift-gen/driver/tchan-driver.go @@ -0,0 +1,101 @@ +// @generated Code generated by thrift-gen. Do not modify. + +// Package driver is generated code used to make or handle TChannel calls using Thrift. +package driver + +import ( + "fmt" + + athrift "github.com/apache/thrift/lib/go/thrift" + "github.com/uber/tchannel-go/thrift" +) + +// Interfaces for the service and client for the services defined in the IDL. + +// TChanDriver is the interface that defines the server handler and client interface. +type TChanDriver interface { + FindNearest(ctx thrift.Context, location string) ([]*DriverLocation, error) +} + +// Implementation of a client and service handler. + +type tchanDriverClient struct { + thriftService string + client thrift.TChanClient +} + +func NewTChanDriverInheritedClient(thriftService string, client thrift.TChanClient) *tchanDriverClient { + return &tchanDriverClient{ + thriftService, + client, + } +} + +// NewTChanDriverClient creates a client that can be used to make remote calls. +func NewTChanDriverClient(client thrift.TChanClient) TChanDriver { + return NewTChanDriverInheritedClient("Driver", client) +} + +func (c *tchanDriverClient) FindNearest(ctx thrift.Context, location string) ([]*DriverLocation, error) { + var resp DriverFindNearestResult + args := DriverFindNearestArgs{ + Location: location, + } + success, err := c.client.Call(ctx, c.thriftService, "findNearest", &args, &resp) + if err == nil && !success { + } + + return resp.GetSuccess(), err +} + +type tchanDriverServer struct { + handler TChanDriver +} + +// NewTChanDriverServer wraps a handler for TChanDriver so it can be +// registered with a thrift.Server. +func NewTChanDriverServer(handler TChanDriver) thrift.TChanServer { + return &tchanDriverServer{ + handler, + } +} + +func (s *tchanDriverServer) Service() string { + return "Driver" +} + +func (s *tchanDriverServer) Methods() []string { + return []string{ + "findNearest", + } +} + +func (s *tchanDriverServer) Handle(ctx thrift.Context, methodName string, protocol athrift.TProtocol) (bool, athrift.TStruct, error) { + switch methodName { + case "findNearest": + return s.handleFindNearest(ctx, protocol) + + default: + return false, nil, fmt.Errorf("method %v not found in service %v", methodName, s.Service()) + } +} + +func (s *tchanDriverServer) handleFindNearest(ctx thrift.Context, protocol athrift.TProtocol) (bool, athrift.TStruct, error) { + var req DriverFindNearestArgs + var res DriverFindNearestResult + + if err := req.Read(protocol); err != nil { + return false, nil, err + } + + r, err := + s.handler.FindNearest(ctx, req.Location) + + if err != nil { + return false, nil, err + } else { + res.Success = r + } + + return err == nil, &res, nil +} diff --git a/example/hotrod/hotrod/services/driver/thrift-gen/driver/ttypes.go b/example/hotrod/hotrod/services/driver/thrift-gen/driver/ttypes.go new file mode 100644 index 00000000..2f5093b1 --- /dev/null +++ b/example/hotrod/hotrod/services/driver/thrift-gen/driver/ttypes.go @@ -0,0 +1,154 @@ +// Autogenerated by Thrift Compiler (0.9.3) +// DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING + +package driver + +import ( + "bytes" + "fmt" + "github.com/apache/thrift/lib/go/thrift" +) + +// (needed to ensure safety because of naive import list construction.) +var _ = thrift.ZERO +var _ = fmt.Printf +var _ = bytes.Equal + +var GoUnusedProtection__ int + +// Attributes: +// - DriverID +// - Location +type DriverLocation struct { + DriverID string `thrift:"driver_id,1,required" json:"driver_id"` + Location string `thrift:"location,2,required" json:"location"` +} + +func NewDriverLocation() *DriverLocation { + return &DriverLocation{} +} + +func (p *DriverLocation) GetDriverID() string { + return p.DriverID +} + +func (p *DriverLocation) GetLocation() string { + return p.Location +} +func (p *DriverLocation) Read(iprot thrift.TProtocol) error { + if _, err := iprot.ReadStructBegin(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T read error: ", p), err) + } + + var issetDriverID bool = false + var issetLocation bool = false + + for { + _, fieldTypeId, fieldId, err := iprot.ReadFieldBegin() + if err != nil { + return thrift.PrependError(fmt.Sprintf("%T field %d read error: ", p, fieldId), err) + } + if fieldTypeId == thrift.STOP { + break + } + switch fieldId { + case 1: + if err := p.readField1(iprot); err != nil { + return err + } + issetDriverID = true + case 2: + if err := p.readField2(iprot); err != nil { + return err + } + issetLocation = true + default: + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + if err := iprot.ReadFieldEnd(); err != nil { + return err + } + } + if err := iprot.ReadStructEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err) + } + if !issetDriverID { + return thrift.NewTProtocolExceptionWithType(thrift.INVALID_DATA, fmt.Errorf("Required field DriverID is not set")) + } + if !issetLocation { + return thrift.NewTProtocolExceptionWithType(thrift.INVALID_DATA, fmt.Errorf("Required field Location is not set")) + } + return nil +} + +func (p *DriverLocation) readField1(iprot thrift.TProtocol) error { + if v, err := iprot.ReadString(); err != nil { + return thrift.PrependError("error reading field 1: ", err) + } else { + p.DriverID = v + } + return nil +} + +func (p *DriverLocation) readField2(iprot thrift.TProtocol) error { + if v, err := iprot.ReadString(); err != nil { + return thrift.PrependError("error reading field 2: ", err) + } else { + p.Location = v + } + return nil +} + +func (p *DriverLocation) Write(oprot thrift.TProtocol) error { + if err := oprot.WriteStructBegin("DriverLocation"); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) + } + if err := p.writeField1(oprot); err != nil { + return err + } + if err := p.writeField2(oprot); err != nil { + return err + } + if err := oprot.WriteFieldStop(); err != nil { + return thrift.PrependError("write field stop error: ", err) + } + if err := oprot.WriteStructEnd(); err != nil { + return thrift.PrependError("write struct stop error: ", err) + } + return nil +} + +func (p *DriverLocation) writeField1(oprot thrift.TProtocol) (err error) { + if err := oprot.WriteFieldBegin("driver_id", thrift.STRING, 1); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field begin error 1:driver_id: ", p), err) + } + if err := oprot.WriteString(string(p.DriverID)); err != nil { + return thrift.PrependError(fmt.Sprintf("%T.driver_id (1) field write error: ", p), err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field end error 1:driver_id: ", p), err) + } + return err +} + +func (p *DriverLocation) writeField2(oprot thrift.TProtocol) (err error) { + if err := oprot.WriteFieldBegin("location", thrift.STRING, 2); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field begin error 2:location: ", p), err) + } + if err := oprot.WriteString(string(p.Location)); err != nil { + return thrift.PrependError(fmt.Sprintf("%T.location (2) field write error: ", p), err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field end error 2:location: ", p), err) + } + return err +} + +func (p *DriverLocation) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("DriverLocation(%+v)", *p) +} diff --git a/example/hotrod/hotrod/services/frontend/best_eta.go b/example/hotrod/hotrod/services/frontend/best_eta.go new file mode 100644 index 00000000..a1094fc7 --- /dev/null +++ b/example/hotrod/hotrod/services/frontend/best_eta.go @@ -0,0 +1,135 @@ +// 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" + + "github.com/opentracing/opentracing-go" + "go.uber.org/zap" + + "github.com/rnburn/hotrod-docker/pkg/log" + "github.com/rnburn/hotrod-docker/pkg/pool" + "github.com/rnburn/hotrod-docker/services/config" + "github.com/rnburn/hotrod-docker/services/customer" + "github.com/rnburn/hotrod-docker/services/driver" + "github.com/rnburn/hotrod-docker/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 opentracing.Tracer, logger log.Factory) *bestETA { + return &bestETA{ + customer: customer.NewClient( + tracer, + logger.With(zap.String("component", "customer_client")), + ), + driver: driver.NewClient( + tracer, + logger.With(zap.String("component", "driver_client")), + ), + route: route.NewClient( + tracer, + logger.With(zap.String("component", "route_client")), + ), + pool: pool.New(config.RouteWorkerPoolSize), + logger: logger, + } +} + +func (eta *bestETA) Get(ctx context.Context, customerID string) (*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)) + + if span := opentracing.SpanFromContext(ctx); span != nil { + span.SetBaggageItem("customer", customer.Name) + } + + 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/example/hotrod/hotrod/services/frontend/server.go b/example/hotrod/hotrod/services/frontend/server.go new file mode 100644 index 00000000..2a338e23 --- /dev/null +++ b/example/hotrod/hotrod/services/frontend/server.go @@ -0,0 +1,95 @@ +// 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 ( + "encoding/json" + "net/http" + + "github.com/opentracing/opentracing-go" + "go.uber.org/zap" + + "github.com/rnburn/hotrod-docker/pkg/httperr" + "github.com/rnburn/hotrod-docker/pkg/log" + "github.com/rnburn/hotrod-docker/pkg/tracing" +) + +// Server implements jaeger-demo-frontend service +type Server struct { + hostPort string + tracer opentracing.Tracer + logger log.Factory + bestETA *bestETA +} + +// NewServer creates a new frontend.Server +func NewServer(hostPort string, tracer opentracing.Tracer, logger log.Factory) *Server { + return &Server{ + hostPort: hostPort, + tracer: tracer, + logger: logger, + bestETA: newBestETA(tracer, logger), + } +} + +// Run starts the frontend server +func (s *Server) Run() error { + mux := s.createServeMux() + s.logger.Bg().Info("Starting", zap.String("address", "http://"+s.hostPort)) + return http.ListenAndServe(s.hostPort, mux) +} + +func (s *Server) createServeMux() http.Handler { + mux := tracing.NewServeMux(s.tracer) + mux.Handle("/", http.HandlerFunc(s.home)) + mux.Handle("/dispatch", http.HandlerFunc(s.dispatch)) + return mux +} + +func (s *Server) home(w http.ResponseWriter, r *http.Request) { + s.logger.For(r.Context()).Info("HTTP", zap.String("method", r.Method), zap.Stringer("url", r.URL)) + http.ServeFile(w, r, "services/frontend/web_assets/index.html") +} + +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 + } + + customerID := r.Form.Get("customer") + if customerID == "" { + http.Error(w, "Missing required 'customer' parameter", 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 + } + + 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/example/hotrod/hotrod/services/frontend/web_assets/index.html b/example/hotrod/hotrod/services/frontend/web_assets/index.html new file mode 100644 index 00000000..5db64bad --- /dev/null +++ b/example/hotrod/hotrod/services/frontend/web_assets/index.html @@ -0,0 +1,90 @@ + + + + + HotROD - Rides On Demand + + + + + + + + +
+
+
+

Hot R.O.D.

+

Rides On Demand

+
+
+ Rachel's Floral Designs +
+
+ Trom Chocolatier +
+
+ Japanese Deserts +
+
+ Amazing Coffee Roasters +
+
+
Click on customer name above to order a car.
+
+
+
+ + + + + diff --git a/example/hotrod/hotrod/services/route/client.go b/example/hotrod/hotrod/services/route/client.go new file mode 100644 index 00000000..3de40a47 --- /dev/null +++ b/example/hotrod/hotrod/services/route/client.go @@ -0,0 +1,64 @@ +// 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/http" + "net/url" + + "github.com/opentracing-contrib/go-stdlib/nethttp" + "github.com/opentracing/opentracing-go" + "go.uber.org/zap" + + "github.com/rnburn/hotrod-docker/pkg/log" + "github.com/rnburn/hotrod-docker/pkg/tracing" +) + +// Client is a remote client that implements route.Interface +type Client struct { + tracer opentracing.Tracer + logger log.Factory + client *tracing.HTTPClient +} + +// NewClient creates a new route.Client +func NewClient(tracer opentracing.Tracer, logger log.Factory) *Client { + return &Client{ + tracer: tracer, + logger: logger, + client: &tracing.HTTPClient{ + Client: &http.Client{Transport: &nethttp.Transport{}}, + Tracer: tracer, + }, + } +} + +// 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://127.0.0.1/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/example/hotrod/hotrod/services/route/interface.go b/example/hotrod/hotrod/services/route/interface.go new file mode 100644 index 00000000..f65addce --- /dev/null +++ b/example/hotrod/hotrod/services/route/interface.go @@ -0,0 +1,30 @@ +// 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" +import "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/example/hotrod/hotrod/services/route/server.go b/example/hotrod/hotrod/services/route/server.go new file mode 100644 index 00000000..e4be024a --- /dev/null +++ b/example/hotrod/hotrod/services/route/server.go @@ -0,0 +1,113 @@ +// 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" + + "github.com/opentracing/opentracing-go" + "go.uber.org/zap" + + "github.com/rnburn/hotrod-docker/pkg/delay" + "github.com/rnburn/hotrod-docker/pkg/httperr" + "github.com/rnburn/hotrod-docker/pkg/httpexpvar" + "github.com/rnburn/hotrod-docker/pkg/log" + "github.com/rnburn/hotrod-docker/pkg/tracing" + "github.com/rnburn/hotrod-docker/services/config" +) + +// Server implements Route service +type Server struct { + hostPort string + tracer opentracing.Tracer + logger log.Factory +} + +// NewServer creates a new route.Server +func NewServer(hostPort string, tracer opentracing.Tracer, 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)) + return http.ListenAndServe(s.hostPort, mux) +} + +func (s *Server) createServeMux() http.Handler { + mux := tracing.NewServeMux(s.tracer) + mux.Handle("/route", http.HandlerFunc(s.route)) + mux.Handle("/debug/vars", http.HandlerFunc(httpexpvar.Handler)) + return mux +} + +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/example/hotrod/hotrod/services/route/stats.go b/example/hotrod/hotrod/services/route/stats.go new file mode 100644 index 00000000..22a2037d --- /dev/null +++ b/example/hotrod/hotrod/services/route/stats.go @@ -0,0 +1,48 @@ +// 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/opentracing/opentracing-go" +) + +var routeCalcByCustomer = expvar.NewMap("route.calc.by.customer.sec") +var routeCalcBySession = expvar.NewMap("route.calc.by.session.sec") + +var stats = []struct { + expvar *expvar.Map + baggage string +}{ + {routeCalcByCustomer, "customer"}, + {routeCalcBySession, "session"}, +} + +func updateCalcStats(ctx context.Context, delay time.Duration) { + span := opentracing.SpanFromContext(ctx) + if span == nil { + return + } + delaySec := float64(delay/time.Millisecond) / 1000.0 + for _, s := range stats { + key := span.BaggageItem(s.baggage) + if key != "" { + s.expvar.AddFloat(key, delaySec) + } + } +} diff --git a/example/hotrod/nginx.conf b/example/hotrod/nginx.conf new file mode 100644 index 00000000..1554687c --- /dev/null +++ b/example/hotrod/nginx.conf @@ -0,0 +1,41 @@ +load_module modules/ngx_http_opentracing_module.so; +load_module modules/ngx_http_jaeger_module.so; + +events {} + +http { + jaeger_reporter_local_agent_host_port $JAEGER_AGENT_HOST:$JAEGER_AGENT_PORT; + jaeger_service_name nginx; + jaeger_sampler_type const; + jaeger_sampler_param 1; + opentracing on; + + upstream frontend { + server localhost:8080; + } + + upstream customer { + server localhost:8081; + } + + upstream route { + server localhost:8083; + } + + server { + listen 80; + server_name localhost; + + location / { + proxy_pass http://frontend; + } + + location /customer { + proxy_pass http://customer; + } + + location /route { + proxy_pass http://route; + } + } +} diff --git a/example/hotrod/start.sh b/example/hotrod/start.sh new file mode 100755 index 00000000..301516a1 --- /dev/null +++ b/example/hotrod/start.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +export JAEGER_AGENT_HOST=$JAEGER_PORT_6831_UDP_ADDR +export JAEGER_AGENT_PORT=$JAEGER_PORT_6831_UDP_PORT +/app/hotrod all & +envsubst '\$JAEGER_AGENT_HOST \$JAEGER_AGENT_PORT' < /app/nginx.conf > /etc/nginx/nginx.conf +nginx +while /bin/true; do + sleep 50 +done