Skip to content

Distributed Tracing with OpenTracing API of .NET Core Applications on Kubernetes

Distributed tracing is a great method in order to be able to address and monitor where we have a performance problem in our applications that we designed as microservice architecture.

In other words, it is a necessary method in order to get some answers such as which request go where, how much time do a request spend end-to-end?

In this article, we will talk about the distributed tracing operations of microservices that we developed on kubernetes with .NET Core using the OpenTracing API and the Jaeger tracer.


Let’s assume we are working on an e-commerce system and we host our applications on kubernetes. We have a User API that is responsible for user-related actions. When a new user is registered in the system, an event called “UserRegisteredEvent” is getting published. One of the subscribers of this event is a service that is responsible for sending an activation e-mail to the user to activate his account.

Well, we will perform end-to-end tracing operations on kubernetes of this asynchronous user registration journey using OpenTracing API and Jaeger tracer.

What is OpenTracing and Jaeger?

In a nutshell, I would like to mention about OpenTracing and Jaeger. It is a specification that allows us to add instrumentation to our applications without depending on any vendor. Just like OpenAPI.

Jaeger is a useful OpenTracing compatible tracer developed by Uber Technologies. It allows us to perform distributed tracing operations on our microservice architecture. You can find more detailed information about Jaeger here.

You can follow this documentation for the installation of Jaeger on kubernetes. I followed the development setup for this article.

Jaeger is composed of “Agent“, “Collector” and “Query“. The agent is a network daemon that listens to the trace data from UDP and passes it to the collector. Trace data is called “Span“. The collector processes the trace data, that passed to itself, in a pipeline (validations, indexes, transformations). Then it stores the data according to the type of the component (Elasticsearch, Cassandra and Kafka) which will be selected.

Query, as is evident from its name, is a UI where we can query the relevant trace results.

Okay, Let’s Get Some Coding!

Before we start coding, we need to have some tools for the platform:

  • Message Broker (I will use RabbitMQ)
  • Docker
  • and Kuberentes

NOTE: Since our main topic is not to about the creation of the platform, I will not focus on the installation topics.

First, let’s develop the User API that will responsible for users to register in the system. To do that, let’s create a project like below.

Then, create a class library called “User.Common.Contracts“.

In this library, we will define contracts that will be shared between our applications. Now let’s create an event that will be published when a new user is registered in the system.

After creating the event, let’s add the “User.Common.Contracts” library as a reference to the “User.API” project.

Now let’s create a new folder called “Models” in the “User.API” project, and then also create “Requests” and “Responses” folders under the “Models” folder.

Let’s define the model, that will be used while a user register in the system, in the “Requests” folder as below.

Also in the “Responses” folder, we need to define an internal response wrapper class.

Now we have to create a new folder called “Services” in the “User.API” project. After that, let’s define an interface called “IUserService” in this folder.

We will perform user related business logic operations via this service.

At this point, we will add a service bus to our project via NuGet to perform the messaging operations in a reliable way. I will use the MetroBus library that is a lightweight wrapper of the MassTransit library.

Then we need to add OpenTracing and Jaeger packages to the project to be able to perform distributed tracing operations.

Now we need another new folder. I love structured folder style. Anyway, let’s create a folder called “Implementations” under the “Services” folder and implement the “IUserService” interface as follows.

Well, let’s take look at what we did in the service class.

Trace context is already propagating automatically to other services by Jaeger. So, when you have api-to-api communication and make the necessary configuration, you can trace a request end to end.

At this point, since we are designing our sample project as an event-based communication (api-to-subscriber), we have provided the trace context propagation operations manually. If we look at the trace scope, we can see that we have created a span called the “create-user-async“. After that, we have added a client tag. It is possible to add additional metadata by using tags. Then we have injected the related trace context into the dictionary by using the “TextMapInjectAdapter“.

By completing the user registration process, we have published the “UserRegisteredEvent” together with the tracing keys to the queue via the service bus. After this point, whoever consumes this event, we will be able to trace the whole request flow as long as the tracing keys are used.

Now, we can create a controller. Let’s create a controller called “UsersController” as follow.

Also in the controller, we are performing user registration operations through the “IUserService” interface.

Now, let’s open the “Startup” class and perform the service injection operations as follows.

We initialized the service bus using RabbitMQ and injected it. Then we injected the tracer after configuring it. I used “const” sampler as a sampling type while configuring the tracer. There are also a few sampling options such as “Probabilistic“, “Rate Limiting” and “Remote“. More detailed sampling information is available here. You can change the agent host information with the node IP in your kubernetes environment. If you set up the agent as a sidecar, you will not need to set any information. It will access with default information.

Now API is ready. Let’s go back to our scenario. When a user is registered in the system, we would publish an event. Then we would create a service which sends an activation e-mail, that subscribed to this event, in order be able to user activates his account.

Now we published the event and we can start developing the service which sends an activation e-mail to users.

Developing of Subscriber

To do that, let’s create a new .NET Core console application.

After creating, let’s add the “User.Common.Contracts” library as a reference. Then we need to include MetroBus, OpenTracing and Jaeger to the project via NuGet.

The console application will be a background service that will work as a daemon. Let’s include “Microsoft.Extensions.Hosting” and “Microsoft.Extensions.DependencyInjection” packages via NuGet to configure the app startup and lifetime management.

Also, we need to include “Microsoft.Extensions.Configuration” and “Microsoft.Extensions.Configuration.Json” packages in order to perform configuration management.
First, let’s create a folder called “Common” and then a class called “TracingExtension” into this folder.

If we recall the API side, we have performed trace context propagation operation manually by publishing tracing keys in the event. Now, at this point that we want to create a span in the consumer, we will perform that using the “TracingExtension” class by extracting the tracing keys into the context.

Well, let’s create another new folder called “Consumers” into the root folder, then define a class named “UserActivationConsumer” in this folder.
-> User.Activation.Consumer.Common
—> Common
—> Consumers

At this point, we are performing the subscribe operations to the “UserRegisteredEvent” model that we published in the API. Then we perform the injection operation of the “ITracer” interface.

In the “consume” method, we are creating a scope by using the “TracingExtension” class that we create to perform the trace context propagation operations. With the “user-activation-link-sender-consumer” trace scope, that has a propagated trace context, we will now be able to trace our operations as api-to-subscriber.

Since this service will work as a background service, now let’s return to the root directory and create folders called “Services/Implementations“. Then under the “Implementations” folder, we need to create a class called “BusService” and implement it like below.

In the “BusService” class, we just implemented the start and stop methods.

Let’s edit the “Program” class as follows.

I think that what we have done in the code block above is clear. We are injecting our services by configuring the configuration and dependency injection. We are also initializing the tracer with the “const” type and with the name “User.Activation.Consumer“.

We are registering the consumer with the queue named “user.activation.queue“. The consumer will subscribe to the queue via “UserRegisteredEvent” model.


Well, we are ready to deploy now. I prepared a simple Docker file and Helm chart in order to perform deployment operations. With this chart, I will deploy applications to Azure Kubernetes Service. You can change the chart according to your own environment.

You can reach the chart and docker file that I have prepared from here.


Now we can go to the test phase. First, let’s perform a POST request to the “api/users” endpoint in order be able to create a new user in the system.

With this request, we have started the user registration journey. As in our scenario, the event was published after the user registered.

After publishing the event, the service (user-activation-consumer), that subscribed to the event in order to send the activation email to the user, has performed its related process.

So what happened in this process, let’s have a look at the Jaeger.

If we look at the flow on the Jaeger, this process has 4 depths and 5 spans. The total process took 29.54ms. If we look at the details of the post operations, the request gets into the “create-user-async” method after passing the related action. Also, user registration operations are performing in this method. Then in the “User.Activation.Consumer” service, the activation link sending operations are performing asynchronously.

Although this journey is executing asynchronously, as we can see, we are able to get some answers such as where this process is now, how much time is spent on each process.


As a developer, we can debug and optimize our code or the request’s life cycle that works in a microservice architecture with distributed tracing. With the OpenTracing API, we can provide that our system can be traced flexibly with different tracers without falling into a vendor lock-in situation. Also in this article, I tried to show the propagation process of trace information in a distributed system.



Bu makale toplam (1470) kez okunmuştur.


Published in.NET CoreArchitecturalASP.NET CoreContainerizingMessagingMicroservicesRabbitMQ


  1. coder coder

    eline sağlık güzel makale olmuş.
    application insight üzerindede tüm cycle görebiliyoruz.
    bunun artısı nedir?

    • Teşekkür ederim. OpenTracing uyumlu Microsoft Azure’un managed Application Insight’ını kullanmak da harika bir seçenek. İkisinin de kendisine has yetenekleri mevcut. Seçim tamamen size ve kullanmakta olduğunuz platforma ve neye ihtiyacınız var (async support, open-source yada değil, ) sorusuna göre değişiklik göstermektedir. Bana göre önemli olan tüm cycle’ı görüp yada göremediğiniz.

  2. Turkel Turkel

    Emeğine sağlık. tracing key leri payloada koymak yerine masstransit in send context nin header na koymak daha genel bir cozum olmazmi ?

    • Teşekkür ederim yorumunuz için. Kesinlikle daha iyi olacaktır. Ben sadece explicit bir şekilde göstermek istedim. 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *


This site uses Akismet to reduce spam. Learn how your comment data is processed.