From 6fef196f33af89e5adefec38c6f105ea7a869b11 Mon Sep 17 00:00:00 2001 From: Simon Zeltser Date: Wed, 19 Dec 2018 18:44:13 -0800 Subject: [PATCH] 1) Instrumented cart service with OpenCensus Trace API, exporting traces to Stackdriver 2) Migrated cart service+tests to .NET Core 2.1. This required to update base image to install openssl package --- src/cartservice/Dockerfile | 1 + src/cartservice/Program.cs | 107 ++++++++-------- src/cartservice/cartservice.csproj | 2 + .../cartstore/InstrumentedCartStore.cs | 116 ++++++++++++++++++ src/cartservice/cartstore/RedisCartStore.cs | 13 +- tests/cartservice/cartservice.tests.csproj | 6 +- 6 files changed, 188 insertions(+), 57 deletions(-) create mode 100644 src/cartservice/cartstore/InstrumentedCartStore.cs diff --git a/src/cartservice/Dockerfile b/src/cartservice/Dockerfile index ad2b1c3..a77331a 100644 --- a/src/cartservice/Dockerfile +++ b/src/cartservice/Dockerfile @@ -22,6 +22,7 @@ RUN apk add --no-cache \ libgcc \ libstdc++ \ libintl \ + openssl \ icu WORKDIR /app COPY --from=builder /cartservice . diff --git a/src/cartservice/Program.cs b/src/cartservice/Program.cs index d1f80bb..4b88beb 100644 --- a/src/cartservice/Program.cs +++ b/src/cartservice/Program.cs @@ -13,6 +13,7 @@ // limitations under the License. using System; +using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -21,6 +22,8 @@ using cartservice.interfaces; using CommandLine; using Grpc.Core; using Microsoft.Extensions.Configuration; +using OpenCensus.Exporter.Stackdriver; +using OpenCensus.Trace; namespace cartservice { @@ -30,6 +33,8 @@ namespace cartservice const string REDIS_ADDRESS = "REDIS_ADDR"; const string CART_SERVICE_PORT = "PORT"; + const string PROJECT_ID = "PROJECT_ID"; + [Verb("start", HelpText = "Starts the server listening on provided port")] class ServerOptions { @@ -41,6 +46,9 @@ namespace cartservice [Option('r', "redis", HelpText = "The ip of redis cache")] public string Redis { get; set; } + + [Option("projectId", HelpText = "The ProjectId to which telemetry will flow")] + public string ProjectId { get; set; } } static object StartServer(string host, int port, ICartStore cartStore) @@ -104,55 +112,27 @@ namespace cartservice Console.WriteLine($"Started as process with id {System.Diagnostics.Process.GetCurrentProcess().Id}"); // Set hostname/ip address - string hostname = options.Host; - if (string.IsNullOrEmpty(hostname)) - { - Console.WriteLine($"Reading host address from {CART_SERVICE_ADDRESS} environment variable"); - hostname = Environment.GetEnvironmentVariable(CART_SERVICE_ADDRESS); - if (string.IsNullOrEmpty(hostname)) - { - Console.WriteLine($"Environment variable {CART_SERVICE_ADDRESS} was not set. Setting the host to 0.0.0.0"); - hostname = "0.0.0.0"; - } - } + string hostname = ReadParameter("host address", options.Host, CART_SERVICE_ADDRESS, p => p, "0.0.0.0"); // Set the port - int port = options.Port; - if (options.Port <= 0) + int port = ReadParameter("cart service port", options.Port, CART_SERVICE_PORT, int.Parse, 8080); + + string projectId = ReadParameter("cloud service project id", options.ProjectId, PROJECT_ID, p => p, null); + + // Initialize Stackdriver Exporter - currently for tracing only + if (!string.IsNullOrEmpty(projectId)) { - Console.WriteLine($"Reading cart service port from {CART_SERVICE_PORT} environment variable"); - string portStr = Environment.GetEnvironmentVariable(CART_SERVICE_PORT); - if (string.IsNullOrEmpty(portStr)) - { - Console.WriteLine($"{CART_SERVICE_PORT} environment variable was not set. Setting the port to 8080"); - port = 8080; - } - else - { - port = int.Parse(portStr); - } + var exporter = new StackdriverExporter( + projectId, + Tracing.ExportComponent, + viewManager: null); + exporter.Start(); } // Set redis cache host (hostname+port) - ICartStore cartStore; - string redis = ReadRedisAddress(options.Redis); - - // Redis was specified via command line or environment variable - if (!string.IsNullOrEmpty(redis)) - { - // If you want to start cart store using local cache in process, you can replace the following line with this: - // cartStore = new LocalCartStore(); - cartStore = new RedisCartStore(redis); - - return StartServer(hostname, port, cartStore); - } - else - { - Console.WriteLine("Redis cache host(hostname+port) was not specified. Starting a cart service using local store"); - Console.WriteLine("If you wanted to use Redis Cache as a backup store, you should provide its address via command line or REDIS_ADDRESS environment variable."); - cartStore = new LocalCartStore(); - } + string redis = ReadParameter("redis cache address", options.Redis, REDIS_ADDRESS, p => p, null); + ICartStore cartStore = InstrumentedCartStore.Create(redis); return StartServer(hostname, port, cartStore); }, errs => 1); @@ -163,21 +143,48 @@ namespace cartservice } } - private static string ReadRedisAddress(string address) + /// + /// Reads parameter in the right order + /// + /// Parameter description + /// Value provided from the command line + /// The name of environment variable where it could have been set + /// The method that parses environment variable and returns typed parameter value + /// Parameter's default value - in case other method failed to assign a value + /// The type of the parameter + /// Parameter value read from all the sources in the right order(priorities) + private static T ReadParameter( + string description, + T commandLineValue, + string environmentVariableName, + Func environmentParser, + T defaultValue) { - if (!string.IsNullOrEmpty(address)) + // Command line argument + if(!EqualityComparer.Default.Equals(commandLineValue, default(T))) { - return address; + return commandLineValue; } - Console.WriteLine($"Reading redis cache address from environment variable {REDIS_ADDRESS}"); - string redis = Environment.GetEnvironmentVariable(REDIS_ADDRESS); - if (!string.IsNullOrEmpty(redis)) + // Environment variable + Console.Write($"Reading {description} from environment variable {environmentVariableName}. "); + string envValue = Environment.GetEnvironmentVariable(environmentVariableName); + if (!string.IsNullOrEmpty(envValue)) { - return redis; + try + { + var envTyped = environmentParser(envValue); + Console.WriteLine("Done!"); + return envTyped; + } + catch (Exception) + { + // We assign the default value later on + } } - return null; + Console.WriteLine($"Environment variable {environmentVariableName} was not set. Setting {description} to {defaultValue}"); + return defaultValue; } } } diff --git a/src/cartservice/cartservice.csproj b/src/cartservice/cartservice.csproj index 3fea4a1..de58b34 100644 --- a/src/cartservice/cartservice.csproj +++ b/src/cartservice/cartservice.csproj @@ -14,6 +14,8 @@ + + diff --git a/src/cartservice/cartstore/InstrumentedCartStore.cs b/src/cartservice/cartstore/InstrumentedCartStore.cs new file mode 100644 index 0000000..1f41b0e --- /dev/null +++ b/src/cartservice/cartstore/InstrumentedCartStore.cs @@ -0,0 +1,116 @@ +// Copyright 2018 Google LLC +// +// 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. + +using System; +using System.Threading.Tasks; +using cartservice.interfaces; +using Hipstershop; +using OpenCensus.Trace; +using OpenCensus.Trace.Sampler; + +namespace cartservice.cartstore +{ + /// + /// Wrapper for Cart Store - instrumented with OpenCensus tracing + /// + internal class InstrumentedCartStore : ICartStore + { + private readonly ICartStore cartStore; + private static ITracer tracer = Tracing.Tracer; + private ISpanBuilder initializeSpanBuilder, addItemSpanBuilder, emptySpanBuilder, getItemSpanBuilder, pingSpanBuilder; + + public InstrumentedCartStore(ICartStore cartStore) + { + this.cartStore = cartStore; + + // Create Span Builders for tracing + initializeSpanBuilder = CreateSpanBuilder("Initialize Cart Store"); + addItemSpanBuilder = CreateSpanBuilder("Add Item"); + emptySpanBuilder = CreateSpanBuilder("Empty Cart"); + getItemSpanBuilder = CreateSpanBuilder("Get Cart"); + pingSpanBuilder = CreateSpanBuilder("Ping"); + } + public Task AddItemAsync(string userId, string productId, int quantity) + { + using (var span = addItemSpanBuilder.StartScopedSpan()) + { + return cartStore.AddItemAsync(userId, productId, quantity); + } + } + + public Task EmptyCartAsync(string userId) + { + using (var span = emptySpanBuilder.StartScopedSpan()) + { + return cartStore.EmptyCartAsync(userId); + } + } + + public Task GetCartAsync(string userId) + { + using (var span = getItemSpanBuilder.StartScopedSpan()) + { + return cartStore.GetCartAsync(userId); + } + } + + public Task InitializeAsync() + { + using (var span = initializeSpanBuilder.StartScopedSpan()) + { + return cartStore.InitializeAsync(); + } + } + + public bool Ping() + { + using (var span = pingSpanBuilder.StartScopedSpan()) + { + return cartStore.Ping(); + } + } + + public static ICartStore Create(string redis) + { + ICartStore cartStore; + + // Redis was specified + if (!string.IsNullOrEmpty(redis)) + { + // If you want to start cart store using local cache in process, you can replace the following line with this: + // cartStore = new LocalCartStore(); + cartStore = new RedisCartStore(redis); + } + else + { + Console.WriteLine("Redis cache host(hostname+port) was not specified. Starting a cart service using local store"); + Console.WriteLine("If you wanted to use Redis Cache as a backup store, you should provide its address via command line or REDIS_ADDRESS environment variable."); + cartStore = new LocalCartStore(); + } + + // We create the cart store wrapped with instrumentation + return new InstrumentedCartStore(cartStore); + } + + private static ISpanBuilder CreateSpanBuilder(string spanName) + { + var spanBuilder = tracer + .SpanBuilder(spanName) + .SetRecordEvents(true) + .SetSampler(Samplers.AlwaysSample); + + return spanBuilder; + } + } +} \ No newline at end of file diff --git a/src/cartservice/cartstore/RedisCartStore.cs b/src/cartservice/cartstore/RedisCartStore.cs index bc7b7e8..7eb68cb 100644 --- a/src/cartservice/cartstore/RedisCartStore.cs +++ b/src/cartservice/cartstore/RedisCartStore.cs @@ -22,11 +22,15 @@ using Google.Protobuf; using Grpc.Core; using Hipstershop; using StackExchange.Redis; +using OpenCensus.Trace; +using OpenCensus.Trace.Sampler; namespace cartservice.cartstore { public class RedisCartStore : ICartStore { + private static ITracer tracer = Tracing.Tracer; + private const string CART_FIELD_NAME = "cart"; private const int REDIS_RETRY_NUM = 5; @@ -76,9 +80,10 @@ namespace cartservice.cartstore return; } + tracer.CurrentSpan.AddAnnotation("Connecting to Redis Cache"); Console.WriteLine("Connecting to Redis: " + connectionString); redis = ConnectionMultiplexer.Connect(redisConnectionOptions); - + tracer.CurrentSpan.AddAnnotation("Finished connecting to Redis Cache"); if (redis == null || !redis.IsConnected) { Console.WriteLine("Wasn't able to connect to redis"); @@ -149,7 +154,7 @@ namespace cartservice.cartstore } catch (Exception ex) { - throw new RpcException(new Status(StatusCode.FailedPrecondition, $"Can't access cart storage. {ex}")); + throw new RpcException(new Grpc.Core.Status(StatusCode.FailedPrecondition, $"Can't access cart storage. {ex}")); } } @@ -167,7 +172,7 @@ namespace cartservice.cartstore } catch (Exception ex) { - throw new RpcException(new Status(StatusCode.FailedPrecondition, $"Can't access cart storage. {ex}")); + throw new RpcException(new Grpc.Core.Status(StatusCode.FailedPrecondition, $"Can't access cart storage. {ex}")); } } @@ -194,7 +199,7 @@ namespace cartservice.cartstore } catch (Exception ex) { - throw new RpcException(new Status(StatusCode.FailedPrecondition, $"Can't access cart storage. {ex}")); + throw new RpcException(new Grpc.Core.Status(StatusCode.FailedPrecondition, $"Can't access cart storage. {ex}")); } } diff --git a/tests/cartservice/cartservice.tests.csproj b/tests/cartservice/cartservice.tests.csproj index 4ada297..a24511e 100644 --- a/tests/cartservice/cartservice.tests.csproj +++ b/tests/cartservice/cartservice.tests.csproj @@ -1,14 +1,14 @@ - netcoreapp2.0 + netcoreapp2.1 false - - + +