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
This commit is contained in:
Simon Zeltser 2018-12-19 18:44:13 -08:00
parent d966bc7c5d
commit 6fef196f33
6 changed files with 188 additions and 57 deletions

View file

@ -22,6 +22,7 @@ RUN apk add --no-cache \
libgcc \
libstdc++ \
libintl \
openssl \
icu
WORKDIR /app
COPY --from=builder /cartservice .

View file

@ -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)
/// <summary>
/// Reads parameter in the right order
/// </summary>
/// <param name="description">Parameter description</param>
/// <param name="commandLineValue">Value provided from the command line</param>
/// <param name="environmentVariableName">The name of environment variable where it could have been set</param>
/// <param name="environmentParser">The method that parses environment variable and returns typed parameter value</param>
/// <param name="defaultValue">Parameter's default value - in case other method failed to assign a value</param>
/// <typeparam name="T">The type of the parameter</typeparam>
/// <returns>Parameter value read from all the sources in the right order(priorities)</returns>
private static T ReadParameter<T>(
string description,
T commandLineValue,
string environmentVariableName,
Func<string, T> environmentParser,
T defaultValue)
{
if (!string.IsNullOrEmpty(address))
// Command line argument
if(!EqualityComparer<T>.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;
}
}
}

View file

@ -14,6 +14,8 @@
<PackageReference Include="grpc.tools" Version="1.12.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.1.1" />
<PackageReference Include="OpenCensus" Version="0.1.0-alpha-33381" />
<PackageReference Include="OpenCensus.Exporter.Stackdriver" Version="0.1.0-alpha-33381" />
<PackageReference Include="StackExchange.Redis" Version="1.2.6" />
</ItemGroup>

View file

@ -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
{
/// <summary>
/// Wrapper for Cart Store - instrumented with OpenCensus tracing
/// </summary>
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<Cart> 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;
}
}
}

View file

@ -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}"));
}
}

View file

@ -1,14 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<TargetFramework>netcoreapp2.1</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Google.Protobuf" Version="3.6.0" />
<PackageReference Include="Google.Protobuf.Tools" Version="3.6.0" />
<PackageReference Include="Google.Protobuf" Version="3.6.1" />
<PackageReference Include="Google.Protobuf.Tools" Version="3.6.1" />
<PackageReference Include="Grpc" Version="1.12.0" />
<PackageReference Include="Grpc.Tools" Version="1.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.0" />