diff --git a/.travis.yml b/.travis.yml index 272da23..846a771 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ dist: trusty mono: - latest dotnet: 2.0.0 -os: +os: - linux script: - ./build.sh diff --git a/Examples/async.spans/README.md b/Examples/async.spans/README.md new file mode 100644 index 0000000..74d83dc --- /dev/null +++ b/Examples/async.spans/README.md @@ -0,0 +1,83 @@ +# Basic example showing how to record async spans between PRODUCER and CONSUMER applications + +This document will show how to implement basic PRODUCER and CONSUMER spans using zipkin4net library. + +## Implementation Overview + +We have 3 applications to produce example PRODUCER and CONSUMER spans. + +- `example.message.center` - Stores and pops messages. The messages contain trace information. +- `example.message.producer` - Creates a message with trace information and stores it to `example.message.center`. Logs PRODUCER span to zipkin server. +- `example.message.consumer` - Fetches the message from `example.message.center`. Logs CONSUMER span to zipkin server. + +## Pre-requisites + +- To build the example, you need to install at least [dotnet 2.2](https://dotnet.microsoft.com/download/dotnet-core/2.2) +- To run the examples, you need a live zipkin server. + +## Running the example + +1. Run `example.message.center` app + - On a command line, navigate to `Examples\async.spans\example.message.center` + - Run `dotnet run` + ![example.message.center](images/run-example.message.center.PNG) + +2. Run `example.message.producer` app + - On a command line, navigate to `Examples\async.spans\example.message.producer` + - Run `dotnet run ` + ![example.message.producer](images/run-example.message.producer.PNG) + +3. Run `example.message.consumer` app + - On a command line, navigate to `Examples\async.spans\example.message.consumer` + - Run `dotnet run ` + ![example.message.consumer](images/run-example.message.consumer.PNG) + +4. Check the output + - Go to zipkin UI + - Search for `message.producer` or `message.consumer` as serviceName + - Click one of the search result, it should show the PRODUCER and CONSUMER spans + ![example-output](images/run-example-output.PNG) + +## What to take note on how to create/use PRODUCER and CONSUMER spans + +### PRODUCER spans + +- To make a PRODUCER span, you need to use `ProducerTrace` class +- Example code from [example.message.producer](example.message.producer/Program.cs) + ```csharp + using (var messageProducerTrace = new ProducerTrace("", "")) + { + // TracedActionAsync extension method logs error annotation if exception occurs + await messageProducerTrace.TracedActionAsync(ProduceMessage(messageProducerTrace.Trace.CurrentSpan, text)); + messageProducerTrace.AddAnnotation(Annotations.Tag("sampleProducerTag", "success!")); + } + ``` +- `TracedActionAsync` is used to run the process that is measured to log error annotation in your zipkin trace if exception is thrown. +- Make a way that trace information is passed to the consumer. So in the example, the trace information is part of the message which will be parsed by the consumer application to create CONSUMER spans. +- Also, custom annotations can be added using the ProducerTrace object method `AddAnnotation`. + +### CONSUMER spans + +- To make a CONSUMER span, you need to use `ConsumerTrace` class +- Example code from [example.message.consumer](example.message.consumer/Program.cs) + ```csharp + static async Task ProcessMessage(Message message) + { + // need to supply trace information from producer + using (var messageConsumerTrace = new ConsumerTrace( + serviceName: "", + rpc: "", + encodedTraceId: message.TraceId, + encodedSpanId: message.SpanId, + encodedParentSpanId: message.ParentId, + sampledStr: message.Sampled, + flagsStr: message.Flags.ToString(CultureInfo.InvariantCulture))) + { + await messageConsumerTrace.TracedActionAsync(Task.Delay(600)); // Test delay for mock processing + messageConsumerTrace.AddAnnotation(Annotations.Tag("sampleConsumerTag", "success!")); + } + } + ``` +- In the example PRODUCER application passed the trace information through the `message` object. Using the trace information, CONSUMER span is created. +- `TracedActionAsync` is used to run the process that is measured to log error annotation in your zipkin trace if exception is thrown. +- Also, custom annotations can be added using the ConsumerTrace object method `AddAnnotation`. diff --git a/Examples/async.spans/example.message.center/Controllers/MessagesController.cs b/Examples/async.spans/example.message.center/Controllers/MessagesController.cs new file mode 100644 index 0000000..f600504 --- /dev/null +++ b/Examples/async.spans/example.message.center/Controllers/MessagesController.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Linq; +using example.message.common; +using Microsoft.AspNetCore.Mvc; + +namespace example.message.center.Controllers +{ + [ApiController] + [Route("[controller]")] + public class MessagesController : ControllerBase + { + private static readonly Stack Messages = new Stack(); + + [HttpPost] + [Route("pop")] + public Message GetOneMessage() + { + if (!Messages.Any()) + return null; + + return Messages.Pop(); + } + + [HttpPost] + [Route("push")] + public string SaveMessage([FromBody]Message message) + { + Messages.Push(message); + + return "Ok"; + } + } +} diff --git a/Examples/async.spans/example.message.center/Controllers/WelcomeController.cs b/Examples/async.spans/example.message.center/Controllers/WelcomeController.cs new file mode 100644 index 0000000..9e43497 --- /dev/null +++ b/Examples/async.spans/example.message.center/Controllers/WelcomeController.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Mvc; + +namespace example.message.center.Controllers +{ + [ApiController] + [Route("[controller]")] + public class WelcomeController : ControllerBase + { + [HttpGet] + public string Welcome() + { + return "Welcome to Message center!"; + } + } +} diff --git a/Examples/async.spans/example.message.center/Program.cs b/Examples/async.spans/example.message.center/Program.cs new file mode 100644 index 0000000..81f2b55 --- /dev/null +++ b/Examples/async.spans/example.message.center/Program.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; + +namespace example.message.center +{ + public class Program + { + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup(); + } +} diff --git a/Examples/async.spans/example.message.center/Properties/launchSettings.json b/Examples/async.spans/example.message.center/Properties/launchSettings.json new file mode 100644 index 0000000..b2908b5 --- /dev/null +++ b/Examples/async.spans/example.message.center/Properties/launchSettings.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:51589", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "welcome", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "example.message.center": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "welcome", + "applicationUrl": "http://localhost:51589", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Examples/async.spans/example.message.center/Startup.cs b/Examples/async.spans/example.message.center/Startup.cs new file mode 100644 index 0000000..e5e0a20 --- /dev/null +++ b/Examples/async.spans/example.message.center/Startup.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json.Serialization; + +namespace example.message.center +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services + .AddMvc() + .AddJsonOptions(options => { + // send back a ISO date + var settings = options.SerializerSettings; + settings.DateFormatHandling = Newtonsoft.Json.DateFormatHandling.IsoDateFormat; + // dont mess with case of properties + var resolver = options.SerializerSettings.ContractResolver as DefaultContractResolver; + resolver.NamingStrategy = null; + }) + .SetCompatibilityVersion(CompatibilityVersion.Version_2_2); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseMvc(); + } + } +} diff --git a/Examples/async.spans/example.message.center/appsettings.Development.json b/Examples/async.spans/example.message.center/appsettings.Development.json new file mode 100644 index 0000000..e203e94 --- /dev/null +++ b/Examples/async.spans/example.message.center/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/Examples/async.spans/example.message.center/appsettings.json b/Examples/async.spans/example.message.center/appsettings.json new file mode 100644 index 0000000..def9159 --- /dev/null +++ b/Examples/async.spans/example.message.center/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Examples/async.spans/example.message.center/example.message.center.csproj b/Examples/async.spans/example.message.center/example.message.center.csproj new file mode 100644 index 0000000..61badb7 --- /dev/null +++ b/Examples/async.spans/example.message.center/example.message.center.csproj @@ -0,0 +1,18 @@ + + + + netcoreapp2.2 + InProcess + + + + + + + + + + + + + diff --git a/Examples/async.spans/example.message.common/Message.cs b/Examples/async.spans/example.message.common/Message.cs new file mode 100644 index 0000000..fe7590e --- /dev/null +++ b/Examples/async.spans/example.message.common/Message.cs @@ -0,0 +1,26 @@ +using System.Runtime.Serialization; + +namespace example.message.common +{ + [DataContract(Name = "message")] + public class Message + { + [DataMember(Name = "text")] + public string Text { get; set; } + + [DataMember(Name = "trace_id")] + public string TraceId { get; set; } + + [DataMember(Name = "parent_id")] + public string ParentId { get; set; } + + [DataMember(Name = "span_id")] + public string SpanId { get; set; } + + [DataMember(Name = "sampled")] + public string Sampled { get; set; } + + [DataMember(Name = "flags")] + public long Flags { get; set; } + } +} diff --git a/Examples/async.spans/example.message.common/ZipkinConsoleLogger.cs b/Examples/async.spans/example.message.common/ZipkinConsoleLogger.cs new file mode 100644 index 0000000..0030870 --- /dev/null +++ b/Examples/async.spans/example.message.common/ZipkinConsoleLogger.cs @@ -0,0 +1,23 @@ +using System; +using zipkin4net; + +namespace example.message.common +{ + public class ZipkinConsoleLogger : ILogger + { + public void LogError(string message) + { + Console.WriteLine(message); + } + + public void LogInformation(string message) + { + Console.WriteLine(message); + } + + public void LogWarning(string message) + { + Console.WriteLine(message); + } + } +} diff --git a/Examples/async.spans/example.message.common/ZipkinHelper.cs b/Examples/async.spans/example.message.common/ZipkinHelper.cs new file mode 100644 index 0000000..aa6b316 --- /dev/null +++ b/Examples/async.spans/example.message.common/ZipkinHelper.cs @@ -0,0 +1,23 @@ +using zipkin4net; +using zipkin4net.Tracers.Zipkin; +using zipkin4net.Transport.Http; + +namespace example.message.common +{ + public static class ZipkinHelper + { + public static void StartZipkin(string zipkinServer) + { + TraceManager.SamplingRate = 1.0f; + var httpSender = new HttpZipkinSender(zipkinServer, "application/json"); + var tracer = new ZipkinTracer(httpSender, new JSONSpanSerializer()); + TraceManager.RegisterTracer(tracer); + TraceManager.Start(new ZipkinConsoleLogger()); + } + + public static void StopZipkin() + { + TraceManager.Stop(); + } + } +} diff --git a/Examples/async.spans/example.message.common/example.message.common.csproj b/Examples/async.spans/example.message.common/example.message.common.csproj new file mode 100644 index 0000000..8872d5d --- /dev/null +++ b/Examples/async.spans/example.message.common/example.message.common.csproj @@ -0,0 +1,11 @@ + + + + netstandard2.0 + + + + + + + diff --git a/Examples/async.spans/example.message.consumer/Program.cs b/Examples/async.spans/example.message.consumer/Program.cs new file mode 100644 index 0000000..b1b4ac1 --- /dev/null +++ b/Examples/async.spans/example.message.consumer/Program.cs @@ -0,0 +1,71 @@ +using example.message.common; +using Newtonsoft.Json; +using System; +using System.Globalization; +using System.Net.Http; +using System.Threading.Tasks; +using zipkin4net; + +namespace example.message.consumer +{ + class Program + { + static async Task Main(string[] args) + { + // setup + var zipkinServer = args[0]; + ZipkinHelper.StartZipkin(zipkinServer); + + // message fetch + var message = await FetchAMessage(); + + if (message != null) + { + + await ProcessMessage(message); + Console.WriteLine($"Message '{message.Text}' was processed!"); + } + else + { + Console.WriteLine($"No messages available!"); + } + + // teardown + ZipkinHelper.StopZipkin(); + Console.ReadKey(); + } + + static async Task ProcessMessage(Message message) + { + // need to supply trace information from producer + using (var messageConsumerTrace = new ConsumerTrace( + serviceName: "message.consumer", + rpc: "process message", + encodedTraceId: message.TraceId, + encodedSpanId: message.SpanId, + encodedParentSpanId: message.ParentId, + sampledStr: message.Sampled, + flagsStr: message.Flags.ToString(CultureInfo.InvariantCulture))) + { + await messageConsumerTrace.TracedActionAsync(Task.Delay(600)); // Test delay for mock processing + messageConsumerTrace.AddAnnotation(Annotations.Tag("sampleConsumerTag", "success!")); + } + } + + static async Task FetchAMessage() + { + var client = new HttpClient + { + BaseAddress = new Uri("http://localhost:51589") + }; + + var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Post, "messages/pop")); + var content = response.Content.ReadAsStringAsync().Result; + + if (string.IsNullOrEmpty(content)) + return null; + else + return JsonConvert.DeserializeObject(content); + } + } +} diff --git a/Examples/async.spans/example.message.consumer/Properties/launchSettings.json b/Examples/async.spans/example.message.consumer/Properties/launchSettings.json new file mode 100644 index 0000000..651a6d7 --- /dev/null +++ b/Examples/async.spans/example.message.consumer/Properties/launchSettings.json @@ -0,0 +1,7 @@ +{ + "profiles": { + "example.message.consumer": { + "commandName": "Project" + } + } +} \ No newline at end of file diff --git a/Examples/async.spans/example.message.consumer/example.message.consumer.csproj b/Examples/async.spans/example.message.consumer/example.message.consumer.csproj new file mode 100644 index 0000000..8bdd708 --- /dev/null +++ b/Examples/async.spans/example.message.consumer/example.message.consumer.csproj @@ -0,0 +1,16 @@ + + + + Exe + netcoreapp2.2 + + + + + + + + + + + diff --git a/Examples/async.spans/example.message.producer/Program.cs b/Examples/async.spans/example.message.producer/Program.cs new file mode 100644 index 0000000..be1d8e8 --- /dev/null +++ b/Examples/async.spans/example.message.producer/Program.cs @@ -0,0 +1,65 @@ +using example.message.common; +using Newtonsoft.Json; +using System; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using zipkin4net; +using zipkin4net.Propagation; + +namespace example.message.producer +{ + class Program + { + static async Task Main(string[] args) + { + // setup + var zipkinServer = args[0]; + ZipkinHelper.StartZipkin(zipkinServer); + + // message sending + var text = Guid.NewGuid().ToString(); + // need to create current trace before using ProducerTrace + Trace.Current = Trace.Create(); + await TracedProduceMessage(text); + Console.WriteLine($"Message '{text}' sent to message center!"); + + // teardown + ZipkinHelper.StopZipkin(); + Console.ReadKey(); + } + + static async Task TracedProduceMessage(string text) + { + using (var messageProducerTrace = new ProducerTrace("message.producer", "create message")) + { + // TracedActionAsync extension method logs error annotation if exception occurs + await messageProducerTrace.TracedActionAsync(ProduceMessage(messageProducerTrace.Trace.CurrentSpan, text)); + messageProducerTrace.AddAnnotation(Annotations.Tag("sampleProducerTag", "success!")); + } + } + + static async Task ProduceMessage(ITraceContext traceContext, string text) + { + var client = new HttpClient + { + BaseAddress = new Uri("http://localhost:51589") + }; + + var message = new Message + { + Text = text, + TraceId = traceContext.SerializeTraceId(), + SpanId = traceContext.SerializeSpanId(), + Sampled = traceContext.SerializeSampledKey(), + Flags = long.Parse(traceContext.SerializeDebugKey()) + }; + var stringContent = JsonConvert.SerializeObject(message); + + await client.SendAsync(new HttpRequestMessage(HttpMethod.Post, "messages/push") + { + Content = new StringContent(stringContent, Encoding.UTF8, "application/json") + }); ; + } + } +} diff --git a/Examples/async.spans/example.message.producer/Properties/launchSettings.json b/Examples/async.spans/example.message.producer/Properties/launchSettings.json new file mode 100644 index 0000000..4b50d0c --- /dev/null +++ b/Examples/async.spans/example.message.producer/Properties/launchSettings.json @@ -0,0 +1,7 @@ +{ + "profiles": { + "example.message.producer": { + "commandName": "Project" + } + } +} \ No newline at end of file diff --git a/Examples/async.spans/example.message.producer/example.message.producer.csproj b/Examples/async.spans/example.message.producer/example.message.producer.csproj new file mode 100644 index 0000000..8bdd708 --- /dev/null +++ b/Examples/async.spans/example.message.producer/example.message.producer.csproj @@ -0,0 +1,16 @@ + + + + Exe + netcoreapp2.2 + + + + + + + + + + + diff --git a/Examples/async.spans/images/run-example-output.PNG b/Examples/async.spans/images/run-example-output.PNG new file mode 100644 index 0000000..c13d1bc Binary files /dev/null and b/Examples/async.spans/images/run-example-output.PNG differ diff --git a/Examples/async.spans/images/run-example.message.center.PNG b/Examples/async.spans/images/run-example.message.center.PNG new file mode 100644 index 0000000..ef23355 Binary files /dev/null and b/Examples/async.spans/images/run-example.message.center.PNG differ diff --git a/Examples/async.spans/images/run-example.message.consumer.PNG b/Examples/async.spans/images/run-example.message.consumer.PNG new file mode 100644 index 0000000..778e108 Binary files /dev/null and b/Examples/async.spans/images/run-example.message.consumer.PNG differ diff --git a/Examples/async.spans/images/run-example.message.producer.PNG b/Examples/async.spans/images/run-example.message.producer.PNG new file mode 100644 index 0000000..83f9561 Binary files /dev/null and b/Examples/async.spans/images/run-example.message.producer.PNG differ diff --git a/README.md b/README.md index 153837e..e3b9f45 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,8 @@ Zipkin is designed to handle complete spans. However, an incorrect usage of the - ServerRecv and ServerSend annotations - ClientSend and ClientRecv annotations - LocalOperationStart and LocalOperationStop annotations +- ProducerStart and ProducerStop annotations +- ConsumerStart and ConsumerStop annotations ### When are my traces/spans sent? @@ -114,6 +116,8 @@ A span is sent asynchronously to the zipkin collector when one of the following - ServerSend - ClientRecv - LocalOperationStop +- ProducerStop +- ConsumerStop with the matching opening annotation specified above (matching means same (traceId, spanId, parentSpanId)). diff --git a/Src/zipkin4net.middleware.aspnetcore/Src/TracingMiddleware.cs b/Src/zipkin4net.middleware.aspnetcore/Src/TracingMiddleware.cs index 11c6b33..4911fb3 100644 --- a/Src/zipkin4net.middleware.aspnetcore/Src/TracingMiddleware.cs +++ b/Src/zipkin4net.middleware.aspnetcore/Src/TracingMiddleware.cs @@ -20,7 +20,7 @@ public static void UseTracing(this IApplicationBuilder app, string serviceName, { var request = context.Request; var traceContext = extractor.Extract(request.Headers); - + var trace = traceContext == null ? Trace.Create() : Trace.CreateFromId(traceContext); Trace.Current = trace; if (routeFilter(request.Path)) @@ -43,4 +43,4 @@ public static void UseTracing(this IApplicationBuilder app, string serviceName, }); } } -} \ No newline at end of file +} diff --git a/Src/zipkin4net/Src/BaseStandardTrace.cs b/Src/zipkin4net/Src/BaseStandardTrace.cs new file mode 100644 index 0000000..77573e1 --- /dev/null +++ b/Src/zipkin4net/Src/BaseStandardTrace.cs @@ -0,0 +1,62 @@ +using zipkin4net.Annotation; +using System; +using System.Threading.Tasks; + +namespace zipkin4net +{ + public class BaseStandardTrace + { + public virtual Trace Trace { internal set; get; } + + public void AddAnnotation(IAnnotation annotation) + { + Trace.Record(annotation); + } + + public virtual void Error(Exception ex) + { + Trace.Record(Annotations.Tag("error", ex.Message)); + } + } + + public static class BaseStandardTraceExtensions + { + /// + /// Runs the task asynchronously with custom return type and + /// adds an error annotation in case of failure + /// + /// + /// + public static async Task TracedActionAsync(this BaseStandardTrace trace, Task task) + { + try + { + return await task; + } + catch (Exception ex) + { + trace?.Error(ex); + throw; + } + } + + /// + /// Runs the task asynchronously and + /// adds an error annotation in case of failure + /// + /// + /// + public static async Task TracedActionAsync(this BaseStandardTrace trace, Task task) + { + try + { + await task; + } + catch (Exception ex) + { + trace?.Error(ex); + throw; + } + } + } +} diff --git a/Src/zipkin4net/Src/ClientTrace.cs b/Src/zipkin4net/Src/ClientTrace.cs index f4ea06e..3ac0931 100644 --- a/Src/zipkin4net/Src/ClientTrace.cs +++ b/Src/zipkin4net/Src/ClientTrace.cs @@ -1,18 +1,14 @@ -using zipkin4net.Annotation; -using System; -using System.IO; -using System.Threading.Tasks; +using System; namespace zipkin4net { - public class ClientTrace : IDisposable + public class ClientTrace : BaseStandardTrace, IDisposable { - public Trace Trace { get; } - public ClientTrace(string serviceName, string rpc) { - if (Trace.Current != null) { - Trace = Trace.Current.Child(); + if (Trace.Current != null) + { + Trace = Trace.Current.Child(); } Trace.Record(Annotations.ClientSend()); @@ -20,40 +16,9 @@ public ClientTrace(string serviceName, string rpc) Trace.Record(Annotations.Rpc(rpc)); } - public void AddAnnotation(IAnnotation annotation) - { - Trace.Record(annotation); - } - - public virtual void Error(Exception ex) - { - Trace?.RecordAnnotation(Annotations.Tag("error", ex.Message)); - } - public void Dispose() { Trace.Record(Annotations.ClientRecv()); } } - - public static class ClientTraceExtensions - { - /// - /// Runs the task asynchronously and adds an error annotation in case of failure - /// - /// - /// - public static async Task TracedActionAsync(this ClientTrace clientTrace, Task task) - { - try - { - return await task; - } - catch (Exception ex) - { - clientTrace?.Error(ex); - throw; - } - } - } -} \ No newline at end of file +} diff --git a/Src/zipkin4net/Src/ConsumerTrace.cs b/Src/zipkin4net/Src/ConsumerTrace.cs new file mode 100644 index 0000000..9c52675 --- /dev/null +++ b/Src/zipkin4net/Src/ConsumerTrace.cs @@ -0,0 +1,35 @@ +using System; +using zipkin4net.Propagation; + +namespace zipkin4net +{ + public class ConsumerTrace : BaseStandardTrace, IDisposable + { + public ConsumerTrace(string serviceName, string rpc, string encodedTraceId, string encodedSpanId, + string encodedParentSpanId, string sampledStr, string flagsStr) + { + var spanState = ExtractorHelper.TryParseTrace(encodedTraceId, encodedSpanId, encodedParentSpanId, + sampledStr, flagsStr); + + if (spanState != default(SpanState)) + { + Trace = Trace.CreateFromId(spanState).Child(); + } + else + { + Trace = Trace.Create(); + } + + Trace.Current = Trace; + + Trace.Record(Annotations.ConsumerStart()); + Trace.Record(Annotations.ServiceName(serviceName)); + Trace.Record(Annotations.Rpc(rpc)); + } + + public void Dispose() + { + Trace.Record(Annotations.ConsumerStop()); + } + } +} diff --git a/Src/zipkin4net/Src/ProducerTrace.cs b/Src/zipkin4net/Src/ProducerTrace.cs new file mode 100644 index 0000000..8699877 --- /dev/null +++ b/Src/zipkin4net/Src/ProducerTrace.cs @@ -0,0 +1,24 @@ +using System; + +namespace zipkin4net +{ + public class ProducerTrace : BaseStandardTrace, IDisposable + { + public ProducerTrace(string serviceName, string rpc) + { + if (Trace.Current != null) + { + Trace = Trace.Current.Child(); + } + + Trace.Record(Annotations.ProducerStart()); + Trace.Record(Annotations.ServiceName(serviceName)); + Trace.Record(Annotations.Rpc(rpc)); + } + + public void Dispose() + { + Trace.Record(Annotations.ProducerStop()); + } + } +} diff --git a/Src/zipkin4net/Src/Propagation/B3Extractor.cs b/Src/zipkin4net/Src/Propagation/B3Extractor.cs index adda489..1b0719c 100644 --- a/Src/zipkin4net/Src/Propagation/B3Extractor.cs +++ b/Src/zipkin4net/Src/Propagation/B3Extractor.cs @@ -1,8 +1,4 @@ -using System; -using zipkin4net.Transport; -using zipkin4net.Utils; - -namespace zipkin4net.Propagation +namespace zipkin4net.Propagation { internal class B3Extractor : IExtractor { @@ -10,8 +6,6 @@ internal class B3Extractor : IExtractor private readonly Getter _getter; private readonly B3SingleExtractor _singleExtractor; - private const int TraceId64BitsSerializationLength = 16; - internal B3Extractor(B3Propagation b3Propagation, Getter getter) { _b3Propagation = b3Propagation; @@ -26,8 +20,8 @@ public ITraceContext Extract(C carrier) { return extracted; } - - return TryParseTrace( + + return ExtractorHelper.TryParseTrace( _getter(carrier, _b3Propagation.TraceIdKey), _getter(carrier, _b3Propagation.SpanIdKey), _getter(carrier, _b3Propagation.ParentSpanIdKey), @@ -35,79 +29,5 @@ public ITraceContext Extract(C carrier) _getter(carrier, _b3Propagation.DebugKey) ); } - - //Internal due to backward compatibility of ZipkinHttpTraceExtractor. Once ZipkinHttpTraceExtractor - //is removed, we can set the visibility to private - internal static ITraceContext TryParseTrace(string encodedTraceId, string encodedSpanId, string encodedParentSpanId, string sampledStr, string flagsStr) - { - if (string.IsNullOrWhiteSpace(encodedTraceId) - || string.IsNullOrWhiteSpace(encodedSpanId)) - { - return default(SpanState); - } - - try - { - var traceIdHigh = ExtractTraceIdHigh(encodedTraceId); - var traceId = ExtractTraceId(encodedTraceId); - var spanId = NumberUtils.DecodeHexString(encodedSpanId); - var parentSpanId = string.IsNullOrWhiteSpace(encodedParentSpanId) ? null : (long?)NumberUtils.DecodeHexString(encodedParentSpanId); - var flags = ZipkinHttpHeaders.ParseFlagsHeader(flagsStr); - var sampled = ZipkinHttpHeaders.ParseSampledHeader(sampledStr); - - if (sampled != null) - { - // When "sampled" header exists, it overrides any existing flags - flags = SpanFlags.SamplingKnown; - if (sampled.Value) - { - flags = flags | SpanFlags.Sampled; - } - } - else - { - if ((flags & SpanFlags.SamplingKnown) == SpanFlags.SamplingKnown) - { - sampled = (flags & SpanFlags.Sampled) == SpanFlags.Sampled; - } - } - - return new SpanState(traceIdHigh, traceId, parentSpanId, spanId, sampled, (flags & SpanFlags.Debug) == SpanFlags.Debug); - } - catch (Exception ex) - { - TraceManager.Logger.LogWarning("Couldn't parse trace context. Trace is ignored. Message:" + ex.Message); - } - - return default(SpanState); - } - - /// - /// Extracts traceIdHigh. Detects if present and then decode the first 16 bytes - /// - private static long ExtractTraceIdHigh(string encodedTraceId) - { - if (encodedTraceId.Length <= TraceId64BitsSerializationLength) - { - return SpanState.NoTraceIdHigh; - } - var traceIdHighLength = encodedTraceId.Length - TraceId64BitsSerializationLength; - var traceIdHighStr = encodedTraceId.Substring(0, traceIdHighLength); - return NumberUtils.DecodeHexString(traceIdHighStr); - } - - /// - /// Extracts traceId. Detects if present and then decode the last 16 bytes - /// - private static long ExtractTraceId(string encodedTraceId) - { - var traceIdLength = TraceId64BitsSerializationLength; - if (encodedTraceId.Length <= TraceId64BitsSerializationLength) - { - traceIdLength = encodedTraceId.Length; - } - var traceIdStartIndex = encodedTraceId.Length - traceIdLength; - return NumberUtils.DecodeHexString(encodedTraceId.Substring(traceIdStartIndex, traceIdLength)); - } } -} \ No newline at end of file +} diff --git a/Src/zipkin4net/Src/Propagation/B3Injector.cs b/Src/zipkin4net/Src/Propagation/B3Injector.cs index 998a0ad..d8331f4 100644 --- a/Src/zipkin4net/Src/Propagation/B3Injector.cs +++ b/Src/zipkin4net/Src/Propagation/B3Injector.cs @@ -1,7 +1,4 @@ -using System.Globalization; -using zipkin4net.Utils; - -namespace zipkin4net.Propagation +namespace zipkin4net.Propagation { internal class B3Injector : IInjector { @@ -16,48 +13,20 @@ public B3Injector(B3Propagation b3Propagation, Setter setter) public void Inject(ITraceContext traceContext, C carrier) { - _setter(carrier, _b3Propagation.TraceIdKey, SerializeTraceId(traceContext)); - _setter(carrier, _b3Propagation.SpanIdKey, NumberUtils.EncodeLongToLowerHexString(traceContext.SpanId)); + _setter(carrier, _b3Propagation.TraceIdKey, traceContext.SerializeTraceId()); + _setter(carrier, _b3Propagation.SpanIdKey, traceContext.SerializeSpanId()); if (traceContext.ParentSpanId != null) { // Cannot be null in theory, the root span must have been created on request receive hence further RPC calls are necessary children - _setter(carrier, _b3Propagation.ParentSpanIdKey, NumberUtils.EncodeLongToLowerHexString(traceContext.ParentSpanId.Value)); + _setter(carrier, _b3Propagation.ParentSpanIdKey, traceContext.SerializeParentSpanId()); } - _setter(carrier, _b3Propagation.DebugKey, ((long)GetFlags(traceContext.Sampled, traceContext.Debug)).ToString(CultureInfo.InvariantCulture)); + _setter(carrier, _b3Propagation.DebugKey, traceContext.SerializeDebugKey()); // Add "Sampled" header for compatibility with Finagle if (traceContext.Sampled.HasValue) { - _setter(carrier, _b3Propagation.SampledKey, traceContext.Sampled.Value ? "1" : "0"); - } - } - - private static string SerializeTraceId(ITraceContext spanState) - { - var hexTraceId = NumberUtils.EncodeLongToLowerHexString(spanState.TraceId); - if (spanState.TraceIdHigh == SpanState.NoTraceIdHigh) - { - return hexTraceId; - } - return NumberUtils.EncodeLongToLowerHexString(spanState.TraceIdHigh) + hexTraceId; - } - - private static SpanFlags GetFlags(bool? isSampled, bool isDebug) - { - var flags = SpanFlags.None; - if (isSampled.HasValue) - { - flags |= SpanFlags.SamplingKnown; - if (isSampled.Value) - { - flags |= SpanFlags.Sampled; - } - } - if (isDebug) - { - flags |= SpanFlags.Debug; + _setter(carrier, _b3Propagation.SampledKey, traceContext.SerializeSampledKey()); } - return flags; } } } diff --git a/Src/zipkin4net/Src/Propagation/ExtractorHelper.cs b/Src/zipkin4net/Src/Propagation/ExtractorHelper.cs new file mode 100644 index 0000000..ebc32df --- /dev/null +++ b/Src/zipkin4net/Src/Propagation/ExtractorHelper.cs @@ -0,0 +1,85 @@ +using System; +using zipkin4net.Transport; +using zipkin4net.Utils; + +namespace zipkin4net.Propagation +{ + internal static class ExtractorHelper + { + private const int TraceId64BitsSerializationLength = 16; + + //Internal due to backward compatibility of ZipkinHttpTraceExtractor. Once ZipkinHttpTraceExtractor + //is removed, we can set the visibility to private + internal static ITraceContext TryParseTrace(string encodedTraceId, string encodedSpanId, string encodedParentSpanId, string sampledStr, string flagsStr) + { + if (string.IsNullOrWhiteSpace(encodedTraceId) + || string.IsNullOrWhiteSpace(encodedSpanId)) + { + return default(SpanState); + } + + try + { + var traceIdHigh = ExtractTraceIdHigh(encodedTraceId); + var traceId = ExtractTraceId(encodedTraceId); + var spanId = NumberUtils.DecodeHexString(encodedSpanId); + var parentSpanId = string.IsNullOrWhiteSpace(encodedParentSpanId) ? null : (long?)NumberUtils.DecodeHexString(encodedParentSpanId); + var flags = ZipkinHttpHeaders.ParseFlagsHeader(flagsStr); + var sampled = ZipkinHttpHeaders.ParseSampledHeader(sampledStr); + + if (sampled != null) + { + // When "sampled" header exists, it overrides any existing flags + flags = SpanFlags.SamplingKnown; + if (sampled.Value) + { + flags |= SpanFlags.Sampled; + } + } + else + { + if ((flags & SpanFlags.SamplingKnown) == SpanFlags.SamplingKnown) + { + sampled = (flags & SpanFlags.Sampled) == SpanFlags.Sampled; + } + } + + return new SpanState(traceIdHigh, traceId, parentSpanId, spanId, sampled, (flags & SpanFlags.Debug) == SpanFlags.Debug); + } + catch (Exception ex) + { + TraceManager.Logger.LogWarning("Couldn't parse trace context. Trace is ignored. Message:" + ex.Message); + } + + return default(SpanState); + } + + /// + /// Extracts traceIdHigh. Detects if present and then decode the first 16 bytes + /// + private static long ExtractTraceIdHigh(string encodedTraceId) + { + if (encodedTraceId.Length <= TraceId64BitsSerializationLength) + { + return SpanState.NoTraceIdHigh; + } + var traceIdHighLength = encodedTraceId.Length - TraceId64BitsSerializationLength; + var traceIdHighStr = encodedTraceId.Substring(0, traceIdHighLength); + return NumberUtils.DecodeHexString(traceIdHighStr); + } + + /// + /// Extracts traceId. Detects if present and then decode the last 16 bytes + /// + private static long ExtractTraceId(string encodedTraceId) + { + var traceIdLength = TraceId64BitsSerializationLength; + if (encodedTraceId.Length <= TraceId64BitsSerializationLength) + { + traceIdLength = encodedTraceId.Length; + } + var traceIdStartIndex = encodedTraceId.Length - traceIdLength; + return NumberUtils.DecodeHexString(encodedTraceId.Substring(traceIdStartIndex, traceIdLength)); + } + } +} diff --git a/Src/zipkin4net/Src/Propagation/InjectorHelper.cs b/Src/zipkin4net/Src/Propagation/InjectorHelper.cs new file mode 100644 index 0000000..0ddea3a --- /dev/null +++ b/Src/zipkin4net/Src/Propagation/InjectorHelper.cs @@ -0,0 +1,56 @@ +using System.Globalization; +using zipkin4net.Utils; + +namespace zipkin4net.Propagation +{ + public static class InjectorHelper + { + public static string SerializeTraceId(this ITraceContext spanState) + { + var hexTraceId = NumberUtils.EncodeLongToLowerHexString(spanState.TraceId); + if (spanState.TraceIdHigh == SpanState.NoTraceIdHigh) + { + return hexTraceId; + } + return NumberUtils.EncodeLongToLowerHexString(spanState.TraceIdHigh) + hexTraceId; + } + + public static string SerializeSpanId(this ITraceContext spanState) + { + return NumberUtils.EncodeLongToLowerHexString(spanState.SpanId); + } + + public static string SerializeParentSpanId(this ITraceContext spanState) + { + return NumberUtils.EncodeLongToLowerHexString(spanState.ParentSpanId.Value); + } + + public static string SerializeDebugKey(this ITraceContext spanState) + { + return ((long)GetFlags(spanState.Sampled, spanState.Debug)).ToString(CultureInfo.InvariantCulture); + } + + public static string SerializeSampledKey(this ITraceContext spanState) + { + return spanState.Sampled.Value ? "1" : "0"; + } + + public static SpanFlags GetFlags(bool? isSampled, bool isDebug) + { + var flags = SpanFlags.None; + if (isSampled.HasValue) + { + flags |= SpanFlags.SamplingKnown; + if (isSampled.Value) + { + flags |= SpanFlags.Sampled; + } + } + if (isDebug) + { + flags |= SpanFlags.Debug; + } + return flags; + } + } +} diff --git a/Src/zipkin4net/Src/ServerTrace.cs b/Src/zipkin4net/Src/ServerTrace.cs index dbae739..5e802b4 100644 --- a/Src/zipkin4net/Src/ServerTrace.cs +++ b/Src/zipkin4net/Src/ServerTrace.cs @@ -1,12 +1,10 @@ -using zipkin4net.Annotation; -using System; -using System.Threading.Tasks; +using System; namespace zipkin4net { - public class ServerTrace : IDisposable + public class ServerTrace : BaseStandardTrace, IDisposable { - public Trace Trace + public override Trace Trace { get { @@ -21,40 +19,9 @@ public ServerTrace(string serviceName, string rpc) Trace.Record(Annotations.Rpc(rpc)); } - public void AddAnnotation(IAnnotation annotation) - { - Trace.Record(annotation); - } - public void Dispose() { Trace.Record(Annotations.ServerSend()); } - - public virtual void Error(Exception ex) - { - Trace.RecordAnnotation(Annotations.Tag("error", ex.Message)); - } - } - - public static class ServerTraceExtensions - { - /// - /// Runs the task asynchronously and adds an error annotation in case of failure - /// - /// - /// - public static async Task TracedActionAsync(this ServerTrace serverTrace, Task task) - { - try - { - await task; - } - catch (Exception ex) - { - serverTrace?.Error(ex); - throw; - } - } } -} \ No newline at end of file +} diff --git a/Src/zipkin4net/Src/Transport/ZipkinHttpTraceExtractor.cs b/Src/zipkin4net/Src/Transport/ZipkinHttpTraceExtractor.cs index 12dd8ae..9db8abb 100644 --- a/Src/zipkin4net/Src/Transport/ZipkinHttpTraceExtractor.cs +++ b/Src/zipkin4net/Src/Transport/ZipkinHttpTraceExtractor.cs @@ -31,7 +31,7 @@ public bool TryExtract(TE carrier, Func extractor, out T public static bool TryParseTrace(string encodedTraceId, string encodedSpanId, string encodedParentSpanId, string sampledStr, string flagsStr, out Trace trace) { - var traceContext = B3Extractor.TryParseTrace(encodedTraceId, encodedSpanId, encodedParentSpanId, sampledStr, flagsStr); + var traceContext = ExtractorHelper.TryParseTrace(encodedTraceId, encodedSpanId, encodedParentSpanId, sampledStr, flagsStr); return TryCreateTraceFromTraceContext(traceContext, out trace); } diff --git a/Src/zipkin4net/Tests/T_BaseStandardTrace.cs b/Src/zipkin4net/Tests/T_BaseStandardTrace.cs new file mode 100644 index 0000000..3c9ad60 --- /dev/null +++ b/Src/zipkin4net/Tests/T_BaseStandardTrace.cs @@ -0,0 +1,136 @@ +using NUnit.Framework; +using System.Threading.Tasks; +using System; +using Moq; +using zipkin4net.Dispatcher; +using zipkin4net.Logger; +using zipkin4net.Annotation; + +namespace zipkin4net.UTest +{ + [TestFixture] + internal class T_BaseStandardTrace + { + private Mock dispatcher; + + [SetUp] + public void SetUp() + { + TraceManager.ClearTracers(); + TraceManager.Stop(); + dispatcher = new Mock(); + TraceManager.Start(new VoidLogger(), dispatcher.Object); + } + + [Test] + public void ShouldLogAnnotation() + { + // Arrange + dispatcher + .Setup(h => h.Dispatch(It.IsAny())) + .Returns(true); + + var trace = Trace.Create(); + trace.ForceSampled(); + Trace.Current = trace; + var baseStandardTrace = new BaseStandardTrace + { + Trace = trace + }; + + // Act + baseStandardTrace.AddAnnotation(Annotations.WireSend()); + + // Assert + dispatcher + .Verify(h => + h.Dispatch(It.Is(m => + m.Annotation is WireSend))); + } + + [Test] + public void ExceptionThrownInTracedActionAsyncTShouldAddErrorTagAndRethrow() + { + var trace = Trace.Create(); + trace.ForceSampled(); + Trace.Current = trace; + var baseStandardTrace = new BaseStandardTrace + { + Trace = trace + }; + var ex = new Exception("something bad happened"); + Task task = Task.Run(() => + { + try + { + return 0; + } + finally + { + throw ex; + } + }); + Assert.ThrowsAsync(() => baseStandardTrace.TracedActionAsync(task)); + + VerifyDispatcherRecordedAnnotation(new TagAnnotation("error", ex.Message)); + } + + [Test] + public void ExceptionThrownInTracedActionAsyncTShouldBeRethrownWhenCurrentTraceIsNull() + { + Trace.Current = null; + var baseStandardTrace = new BaseStandardTrace(); + + Task task = Task.Run(() => throw new SomeException()); + + Assert.ThrowsAsync(() => baseStandardTrace.TracedActionAsync(task)); + } + + [Test] + public void ExceptionThrownInTracedActionAsyncShouldAddErrorTagAndRethrow() + { + var trace = Trace.Create(); + trace.ForceSampled(); + Trace.Current = trace; + var baseStandardTrace = new BaseStandardTrace + { + Trace = trace + }; + var ex = new Exception("something bad happened"); + Task task = Task.Run(() => + { + try + { + return; + } + finally + { + throw ex; + } + }); + Assert.ThrowsAsync(() => baseStandardTrace.TracedActionAsync(task)); + + VerifyDispatcherRecordedAnnotation(new TagAnnotation("error", ex.Message)); + } + + [Test] + public void ExceptionThrownInTracedActionAsyncShouldBeRethrownWhenCurrentTraceIsNull() + { + Trace.Current = null; + var baseStandardTrace = new BaseStandardTrace(); + + Task task = Task.Run(() => throw new SomeException()); + + Assert.ThrowsAsync(() => baseStandardTrace.TracedActionAsync(task)); + } + + private void VerifyDispatcherRecordedAnnotation(IAnnotation annotation) + { + dispatcher.Verify(d => d.Dispatch(It.Is(r => r.Annotation.Equals(annotation)))); + } + + private class SomeException : Exception + { + } + } +} \ No newline at end of file diff --git a/Src/zipkin4net/Tests/T_ClientTrace.cs b/Src/zipkin4net/Tests/T_ClientTrace.cs index a5e9829..34209c6 100644 --- a/Src/zipkin4net/Tests/T_ClientTrace.cs +++ b/Src/zipkin4net/Tests/T_ClientTrace.cs @@ -1,6 +1,4 @@ using NUnit.Framework; -using System.Threading.Tasks; -using System; using Moq; using zipkin4net.Dispatcher; using zipkin4net.Logger; @@ -47,47 +45,43 @@ public void ShouldCallChildWhenCurrentTraceNotNull() } [Test] - public void ExceptionThrownInTracedActionAsyncShouldAddErrorTagAndRethrow() + public void ShouldLogClientAnnotations() { + // Arrange + dispatcher + .Setup(h => h.Dispatch(It.IsAny())) + .Returns(true); + + // Act var trace = Trace.Create(); trace.ForceSampled(); Trace.Current = trace; - var clientTrace = new ClientTrace(serviceName, rpc); - var ex = new Exception("something bad happened"); - Task task = Task.Run(() => + using (var client = new ClientTrace(serviceName, rpc)) { - try - { - return 0; - } - finally - { - throw ex; - } - }); - Assert.ThrowsAsync(() => clientTrace.TracedActionAsync(task)); - - VerifyDispatcherRecordedAnnotation(new TagAnnotation("error", ex.Message)); - } - - [Test] - public void ExceptionThrownInTracedActionAsyncShouldBeRethrownWhenCurrentTraceIsNull() - { - Trace.Current = null; - var clientTrace = new ClientTrace(serviceName, rpc); - - Task task = Task.Run(() => throw new SomeException()); + // Assert + dispatcher + .Verify(h => + h.Dispatch(It.Is(m => + m.Annotation is ClientSend))); - Assert.ThrowsAsync(() => clientTrace.TracedActionAsync(task)); - } + dispatcher + .Verify(h => + h.Dispatch(It.Is(m => + m.Annotation is ServiceName + && ((ServiceName)m.Annotation).Service == serviceName))); - private void VerifyDispatcherRecordedAnnotation(IAnnotation annotation) - { - dispatcher.Verify(d => d.Dispatch(It.Is(r => r.Annotation.Equals(annotation)))); - } + dispatcher + .Verify(h => + h.Dispatch(It.Is(m => + m.Annotation is Rpc + && ((Rpc)m.Annotation).Name == rpc))); + } - private class SomeException : Exception - { + // Assert + dispatcher + .Verify(h => + h.Dispatch(It.Is(m => + m.Annotation is ClientRecv))); } } } \ No newline at end of file diff --git a/Src/zipkin4net/Tests/T_ConsumerTrace.cs b/Src/zipkin4net/Tests/T_ConsumerTrace.cs new file mode 100644 index 0000000..2132bc7 --- /dev/null +++ b/Src/zipkin4net/Tests/T_ConsumerTrace.cs @@ -0,0 +1,93 @@ +using NUnit.Framework; +using Moq; +using zipkin4net.Dispatcher; +using zipkin4net.Logger; +using zipkin4net.Annotation; +using zipkin4net.Propagation; + +namespace zipkin4net.UTest +{ + [TestFixture] + internal class T_ConsumerTrace + { + private Mock dispatcher; + private const string serviceName = "service1"; + private const string rpc = "rpc"; + + [SetUp] + public void SetUp() + { + TraceManager.ClearTracers(); + TraceManager.Stop(); + dispatcher = new Mock(); + TraceManager.Start(new VoidLogger(), dispatcher.Object); + } + + [Test] + public void ShouldSetCurrentTraceIfInvalidTraceInformationIsPassed() + { + TraceManager.SamplingRate = 1.0f; + using (var client = new ConsumerTrace(serviceName, rpc, null, null, null, null, null)) + { + Assert.IsNotNull(client.Trace); + } + } + + [Test] + public void ShouldSetChildTraceIfValidTraceInformationIsPassed() + { + TraceManager.SamplingRate = 1.0f; + var rootTrace = Trace.Create(); + var trace = rootTrace.Child(); + var context = trace.CurrentSpan; + using (var client = new ConsumerTrace(serviceName, rpc, + context.SerializeTraceId(), + context.SerializeSpanId(), + context.SerializeParentSpanId(), + context.SerializeSampledKey(), + context.SerializeDebugKey())) + { + Assert.AreEqual(trace.CurrentSpan.SpanId, client.Trace.CurrentSpan.ParentSpanId); + Assert.AreEqual(trace.CurrentSpan.TraceId, client.Trace.CurrentSpan.TraceId); + } + } + + [Test] + public void ShouldLogConsumerAnnotations() + { + // Arrange + dispatcher + .Setup(h => h.Dispatch(It.IsAny())) + .Returns(true); + + // Act + TraceManager.SamplingRate = 1.0f; + using (var server = new ConsumerTrace(serviceName, rpc, null, null, null, null, null)) + { + // Assert + dispatcher + .Verify(h => + h.Dispatch(It.Is(m => + m.Annotation is ConsumerStart))); + + dispatcher + .Verify(h => + h.Dispatch(It.Is(m => + m.Annotation is ServiceName + && ((ServiceName)m.Annotation).Service == serviceName))); + + dispatcher + .Verify(h => + h.Dispatch(It.Is(m => + m.Annotation is Rpc + && ((Rpc)m.Annotation).Name == rpc))); + } + + // Assert + dispatcher + .Verify(h => + h.Dispatch(It.Is(m => + m.Annotation is ConsumerStop))); + } + } +} \ No newline at end of file diff --git a/Src/zipkin4net/Tests/T_ProducerTrace.cs b/Src/zipkin4net/Tests/T_ProducerTrace.cs new file mode 100644 index 0000000..caa5513 --- /dev/null +++ b/Src/zipkin4net/Tests/T_ProducerTrace.cs @@ -0,0 +1,87 @@ +using NUnit.Framework; +using Moq; +using zipkin4net.Dispatcher; +using zipkin4net.Logger; +using zipkin4net.Annotation; + +namespace zipkin4net.UTest +{ + [TestFixture] + internal class T_ProducerTrace + { + private Mock dispatcher; + private const string serviceName = "service1"; + private const string rpc = "rpc"; + + [SetUp] + public void SetUp() + { + TraceManager.ClearTracers(); + TraceManager.Stop(); + dispatcher = new Mock(); + TraceManager.Start(new VoidLogger(), dispatcher.Object); + } + + [Test] + public void ShouldNotSetCurrentTrace() + { + Trace.Current = null; + using (var producer = new ProducerTrace(serviceName, rpc)) + { + Assert.IsNull(producer.Trace); + } + } + + [Test] + public void ShouldCallChildWhenCurrentTraceNotNull() + { + var trace = Trace.Create(); + Trace.Current = trace; + using (var producer = new ProducerTrace(serviceName, rpc)) + { + Assert.AreEqual(trace.CurrentSpan.SpanId, producer.Trace.CurrentSpan.ParentSpanId); + Assert.AreEqual(trace.CurrentSpan.TraceId, producer.Trace.CurrentSpan.TraceId); + } + } + + [Test] + public void ShouldLogProducerAnnotations() + { + // Arrange + dispatcher + .Setup(h => h.Dispatch(It.IsAny())) + .Returns(true); + + // Act + var trace = Trace.Create(); + trace.ForceSampled(); + Trace.Current = trace; + using (var producer = new ProducerTrace(serviceName, rpc)) + { + // Assert + dispatcher + .Verify(h => + h.Dispatch(It.Is(m => + m.Annotation is ProducerStart))); + + dispatcher + .Verify(h => + h.Dispatch(It.Is(m => + m.Annotation is ServiceName + && ((ServiceName)m.Annotation).Service == serviceName))); + + dispatcher + .Verify(h => + h.Dispatch(It.Is(m => + m.Annotation is Rpc + && ((Rpc)m.Annotation).Name == rpc))); + } + + // Assert + dispatcher + .Verify(h => + h.Dispatch(It.Is(m => + m.Annotation is ProducerStop))); + } + } +} \ No newline at end of file diff --git a/Src/zipkin4net/Tests/T_ServerTrace.cs b/Src/zipkin4net/Tests/T_ServerTrace.cs new file mode 100644 index 0000000..6de2609 --- /dev/null +++ b/Src/zipkin4net/Tests/T_ServerTrace.cs @@ -0,0 +1,65 @@ +using NUnit.Framework; +using Moq; +using zipkin4net.Dispatcher; +using zipkin4net.Logger; +using zipkin4net.Annotation; + +namespace zipkin4net.UTest +{ + [TestFixture] + internal class T_ServerTrace + { + private Mock dispatcher; + private const string serviceName = "service1"; + private const string rpc = "rpc"; + + [SetUp] + public void SetUp() + { + TraceManager.ClearTracers(); + TraceManager.Stop(); + dispatcher = new Mock(); + TraceManager.Start(new VoidLogger(), dispatcher.Object); + } + + [Test] + public void ShouldLogServerAnnotations() + { + // Arrange + dispatcher + .Setup(h => h.Dispatch(It.IsAny())) + .Returns(true); + + // Act + var trace = Trace.Create(); + trace.ForceSampled(); + Trace.Current = trace; + using (var server = new ServerTrace(serviceName, rpc)) + { + // Assert + dispatcher + .Verify(h => + h.Dispatch(It.Is(m => + m.Annotation is ServerRecv))); + + dispatcher + .Verify(h => + h.Dispatch(It.Is(m => + m.Annotation is ServiceName + && ((ServiceName)m.Annotation).Service == serviceName))); + + dispatcher + .Verify(h => + h.Dispatch(It.Is(m => + m.Annotation is Rpc + && ((Rpc)m.Annotation).Name == rpc))); + } + + // Assert + dispatcher + .Verify(h => + h.Dispatch(It.Is(m => + m.Annotation is ServerSend))); + } + } +} \ No newline at end of file diff --git a/Src/zipkin4net/Tests/Transport/Http/T_TracingHandler.cs b/Src/zipkin4net/Tests/Transport/Http/T_TracingHandler.cs index dfccc15..6172aad 100644 --- a/Src/zipkin4net/Tests/Transport/Http/T_TracingHandler.cs +++ b/Src/zipkin4net/Tests/Transport/Http/T_TracingHandler.cs @@ -83,7 +83,7 @@ m.Annotation is TagAnnotation } [Test] - public async Task ShouldLogHttpHost() + public async Task ShouldNotLogHttpHost() { // Arrange var returnStatusCode = HttpStatusCode.BadRequest; diff --git a/zipkin4net.sln b/zipkin4net.sln index 81ebd6d..abbf371 100644 --- a/zipkin4net.sln +++ b/zipkin4net.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27130.2010 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29926.136 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zipkin4net", "Src\zipkin4net\Src\zipkin4net.csproj", "{537FF480-331E-4567-AA77-1BAF43F205CB}" EndProject @@ -41,6 +41,16 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "middleware", "middleware", EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{37F98480-6352-42B5-BDD0-421DC4387B87}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "async.spans", "async.spans", "{4749E7F6-CF0D-4501-AF99-5E7C763A1DEF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "example.message.producer", "Examples\async.spans\example.message.producer\example.message.producer.csproj", "{699F4AC2-83E2-4D87-B22E-10C52830738D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "example.message.consumer", "Examples\async.spans\example.message.consumer\example.message.consumer.csproj", "{A5EF561A-F9EE-4687-A2EB-F05250ED940A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "example.message.common", "Examples\async.spans\example.message.common\example.message.common.csproj", "{4541D072-C213-43F3-9D3F-933871733430}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "example.message.center", "Examples\async.spans\example.message.center\example.message.center.csproj", "{F6D38AA9-60D5-4907-B79D-DE5456F693FD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -95,27 +105,48 @@ Global {6C3CEF75-174F-436B-B252-F2F339521307}.Debug|Any CPU.Build.0 = Debug|Any CPU {6C3CEF75-174F-436B-B252-F2F339521307}.Release|Any CPU.ActiveCfg = Release|Any CPU {6C3CEF75-174F-436B-B252-F2F339521307}.Release|Any CPU.Build.0 = Release|Any CPU + {699F4AC2-83E2-4D87-B22E-10C52830738D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {699F4AC2-83E2-4D87-B22E-10C52830738D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {699F4AC2-83E2-4D87-B22E-10C52830738D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {699F4AC2-83E2-4D87-B22E-10C52830738D}.Release|Any CPU.Build.0 = Release|Any CPU + {A5EF561A-F9EE-4687-A2EB-F05250ED940A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A5EF561A-F9EE-4687-A2EB-F05250ED940A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A5EF561A-F9EE-4687-A2EB-F05250ED940A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A5EF561A-F9EE-4687-A2EB-F05250ED940A}.Release|Any CPU.Build.0 = Release|Any CPU + {4541D072-C213-43F3-9D3F-933871733430}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4541D072-C213-43F3-9D3F-933871733430}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4541D072-C213-43F3-9D3F-933871733430}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4541D072-C213-43F3-9D3F-933871733430}.Release|Any CPU.Build.0 = Release|Any CPU + {F6D38AA9-60D5-4907-B79D-DE5456F693FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F6D38AA9-60D5-4907-B79D-DE5456F693FD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F6D38AA9-60D5-4907-B79D-DE5456F693FD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F6D38AA9-60D5-4907-B79D-DE5456F693FD}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution + {537FF480-331E-4567-AA77-1BAF43F205CB} = {37F98480-6352-42B5-BDD0-421DC4387B87} {5BCC39BF-CCB1-450C-8831-389A4B935A4A} = {8DC9F862-EC74-4F2B-BAB1-C445C76E6EC6} - {2E8B6A44-12FD-4432-BDA8-9CFC2B148D48} = {8DC9F862-EC74-4F2B-BAB1-C445C76E6EC6} + {B2591E13-5E59-4D09-BAFD-C285BB6315B9} = {37F98480-6352-42B5-BDD0-421DC4387B87} + {F02EDF71-04C6-44A2-8C11-8F23A7F7A6AC} = {576FE96A-9156-4DFF-BA99-6E3AAC66E3BE} + {654A440D-6490-42F0-BFC3-14086F3817BF} = {576FE96A-9156-4DFF-BA99-6E3AAC66E3BE} {6FFC4FC6-725F-4463-922C-79DCC48DF3D7} = {2E8B6A44-12FD-4432-BDA8-9CFC2B148D48} - {0F072F12-188A-4BB5-857B-B7B3D59C0ED6} = {DD8D68DE-B823-422F-B22B-E28B8FA1397E} + {7DEAB9B8-FC6E-409D-8CA1-39E1FE0990C4} = {9D41A684-7B6C-4F00-972C-BDE4E5ECCAA3} + {5D3A113D-1E11-40AC-AEA5-D5F8FE8292F8} = {9D41A684-7B6C-4F00-972C-BDE4E5ECCAA3} + {9D64A8B7-B8C6-4B52-ACA5-AE2C3A73BE97} = {9D41A684-7B6C-4F00-972C-BDE4E5ECCAA3} {CB09552F-2BE0-453F-8F1E-A3B51CC9667D} = {0F072F12-188A-4BB5-857B-B7B3D59C0ED6} - {6C3CEF75-174F-436B-B252-F2F339521307} = {0F072F12-188A-4BB5-857B-B7B3D59C0ED6} {F92CE013-4974-45D6-973B-0E03D49CE405} = {0F072F12-188A-4BB5-857B-B7B3D59C0ED6} + {6C3CEF75-174F-436B-B252-F2F339521307} = {0F072F12-188A-4BB5-857B-B7B3D59C0ED6} + {2E8B6A44-12FD-4432-BDA8-9CFC2B148D48} = {8DC9F862-EC74-4F2B-BAB1-C445C76E6EC6} + {0F072F12-188A-4BB5-857B-B7B3D59C0ED6} = {DD8D68DE-B823-422F-B22B-E28B8FA1397E} {9D41A684-7B6C-4F00-972C-BDE4E5ECCAA3} = {DD8D68DE-B823-422F-B22B-E28B8FA1397E} - {9D64A8B7-B8C6-4B52-ACA5-AE2C3A73BE97} = {9D41A684-7B6C-4F00-972C-BDE4E5ECCAA3} - {5D3A113D-1E11-40AC-AEA5-D5F8FE8292F8} = {9D41A684-7B6C-4F00-972C-BDE4E5ECCAA3} - {7DEAB9B8-FC6E-409D-8CA1-39E1FE0990C4} = {9D41A684-7B6C-4F00-972C-BDE4E5ECCAA3} - {F02EDF71-04C6-44A2-8C11-8F23A7F7A6AC} = {576FE96A-9156-4DFF-BA99-6E3AAC66E3BE} - {654A440D-6490-42F0-BFC3-14086F3817BF} = {576FE96A-9156-4DFF-BA99-6E3AAC66E3BE} - {537FF480-331E-4567-AA77-1BAF43F205CB} = {37F98480-6352-42B5-BDD0-421DC4387B87} - {B2591E13-5E59-4D09-BAFD-C285BB6315B9} = {37F98480-6352-42B5-BDD0-421DC4387B87} {576FE96A-9156-4DFF-BA99-6E3AAC66E3BE} = {37F98480-6352-42B5-BDD0-421DC4387B87} + {4749E7F6-CF0D-4501-AF99-5E7C763A1DEF} = {DD8D68DE-B823-422F-B22B-E28B8FA1397E} + {699F4AC2-83E2-4D87-B22E-10C52830738D} = {4749E7F6-CF0D-4501-AF99-5E7C763A1DEF} + {A5EF561A-F9EE-4687-A2EB-F05250ED940A} = {4749E7F6-CF0D-4501-AF99-5E7C763A1DEF} + {4541D072-C213-43F3-9D3F-933871733430} = {4749E7F6-CF0D-4501-AF99-5E7C763A1DEF} + {F6D38AA9-60D5-4907-B79D-DE5456F693FD} = {4749E7F6-CF0D-4501-AF99-5E7C763A1DEF} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {624F3E6A-F976-434B-9D83-591B0F35D42B}