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