Exploring Distributed Tracing Using ASP.NET Core And Jaeger

This short article shows how to set up a local testing environment of Jaeger, integrate with ASP.NET Core application and why we need the distributed tracing during our development and deployment.

Introduction

 
Modern software architectures built on microservices and serverless introduce advantages to application development but there’s also the cost of reduced visibility.
 
Requests often span multiple services. Each service handles a request by performing one or more operations, for examaple - database queries, publish messages, etc.
 

How to understand the behavior of an application, monitor services, and troubleshoot problems?

 
Distributed tracing provides end-to-end visibility and reveals service dependencies – showing how the services respond to each other.
 
This article demonstrates how to set up the Jaeger all-in-one deployment on local testing with traces generated from two ASP.NET Core applications.
 

Setup Jaeger

 
Using docker to setup Jaeger is very easy, and jaegertracing provides an image named all-in-one which is designed for quick local testing, launches the Jaeger UI, collector, query, and an agent with an in-memory storage component.
  1. docker run -d --name jaeger \  
  2.   -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \  
  3.   -p 5775:5775/udp \  
  4.   -p 6831:6831/udp \  
  5.   -p 6832:6832/udp \  
  6.   -p 5778:5778 \  
  7.   -p 16686:16686 \  
  8.   -p 14268:14268 \  
  9.   -p 9411:9411 \  
  10.   jaegertracing/all-in-one:latest  

After running up, we can navigate to http://localhost:16686 to access the Jaeger UI.

 
What we should do next is to integrate it with ASP.NET Core.
 

Integrate With ASP.NET Core

 
Here, we will create two ASP.NET Core Web API projects to show - one of them is named CustomerApi and the other one is named OrderApi.
 
We should add two NuGet packages in our projects.
  1. Install-Package Jaeger -Version 0.2.2   
  2.   
  3. Install-Package OpenTracing.Contrib.NetCore -Version 0.5.0  
Each project should add the following code in the ConfigureServices method.
  1. services.AddSingleton<ITracer>(serviceProvider =>  
  2. {  
  3.     string serviceName = Assembly.GetEntryAssembly().GetName().Name;  
  4.   
  5.     ILoggerFactory loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();  
  6.   
  7.     ISampler sampler = new ConstSampler(sample: true);  
  8.   
  9.     ITracer tracer = new Tracer.Builder(serviceName)  
  10.         .WithLoggerFactory(loggerFactory)  
  11.         .WithSampler(sampler)  
  12.         .Build();  
  13.   
  14.     GlobalTracer.Register(tracer);  
  15.   
  16.     return tracer;  
  17. });  
  18.   
  19. services.AddOpenTracing();  
We will query some data from SQLite with EFCore in the OrderApi service. Also, add a method that will throw an exception.
  1. [Route("api/orders")]  
  2. public class OrdersController : Controller  
  3. {  
  4.     private readonly OrderDbContext _dbContext;  
  5.   
  6.     public OrdersController(OrderDbContext dbContext)  
  7.     {  
  8.         this._dbContext = dbContext;  
  9.     }  
  10.   
  11.     // GET: api/orders  
  12.     [HttpGet]  
  13.     public IEnumerable<string> Get()  
  14.     {  
  15.         // query data from SQLite  
  16.         var orders = _dbContext.Orders.ToList();  
  17.         return new string[] { "value1""value2" };  
  18.     }  
  19.   
  20.     // GET: api/orders/1  
  21.     [HttpGet("{id}")]  
  22.     public ActionResult<string> Get(int id)  
  23.     {  
  24.         throw new System.Exception("Error Test");  
  25.         return "value";  
  26.     }  
  27. }  
In CustomerApi service, we will use HttpClient to call OrderApi service so that we can see the tracing between different services.
  1. [Route("api/[controller]")]  
  2. [ApiController]  
  3. public class ValuesController : ControllerBase  
  4. {  
  5.     private readonly IHttpClientFactory _factory;  
  6.   
  7.     public ValuesController(IHttpClientFactory factory)  
  8.     {  
  9.         this._factory = factory;  
  10.     }  
  11.   
  12.     // GET api/values  
  13.     [HttpGet]  
  14.     public async Task<IEnumerable<string>> GetAsync()  
  15.     {  
  16.         var data = await GetSomeThingFromOrderApi();  
  17.   
  18.         return new string[] { "value1""value2" };  
  19.     }  
  20.   
  21.     private async Task<string> GetSomeThingFromOrderApi()  
  22.     {  
  23.         var client = _factory.CreateClient("orderApi");  
  24.   
  25.         var requestMsg = new HttpRequestMessage(HttpMethod.Get, "http://localhost:8990/api/orders");  
  26.   
  27.         var responseMsg  = await client.SendAsync(requestMsg);  
  28.   
  29.         var data = await responseMsg.Content.ReadAsStringAsync();  
  30.   
  31.         return data;  
  32.     }  
  33. }  
After finishing the above steps, we can run those two services up, and visit CustomerApi via http://localhost:8989/api/values.
 
Turn to Jaeger UI and select CustomerApi.
 
 
As you can see, there is some useful information about this request!
  1. HTTP GET request costs 17.78ms 
  2. 8 Spans with two services 
  3. Happen in 10.38.20 pm 
  4. ...
However, that information is an overview of this request. We should learn more details about it!
 
Click this item and we can see more information about this request.
 
 
OrderApi contains a span named DB ExecuteReader, it shows us the statement that EFCore generated. Sometimes, this is a very helpful message that can optimize our applications.
 
 
Visiting http://localhost:8989/api/values one more time. It will generate another record, we can compare those two traces to find out the difference between them.
 
 
Also, we can see those two service dependencies. The following screenshot tells us that CustomerApi is dependent on OrderApi.
 
What about our services throw exceptions?
 
Visiting http://localhost:8990/api/orders/1 and we will learn how it records the exception when our services throw.
 
  
It recorded the full exception message and StackTrace that can help us to find out the problem quickly!!
 

Summary

 
This short article shows how to set up a local testing environment of Jaeger, integrate with ASP.NET Core application and why we need a distributed tracing during our development and deployment.
 
Hope this article can help you!
 
Additional Resources