Usando OpenTracing e Jaeger em serviços distribuídos ASP.net Core

No post anterior, apresentamos OpenTracing e Jaeger. Agora, vamos mostrar como instrumentar serviços distribuídos ASP.net core utilizando essas tecnologias.

Explicando o exemplo

No exemplo desse post, queremos que dois serviços (A e B) operem juntos para atender requisições do usuário. Queremos que o tracing dessas requisições seja persistido no Jaeger.

Todas as requisições são feitas para ServiceA que, por sua vez, chama ServiceB para poder construir uma resposta.

 

A implementação é simples. Toda vez que o usuário fizer uma requisição contra ServiceA (em http://localhost:5002/api/values) este irá fazer uma requisição contra ServiceB (em http://localhost:5003/api/values), materializará a lista e fará o retorno.

Pacotes Nuget que precisam ser instalados nos projetos

Para nossa implementação, precisaremos instalar três pacotes Nuget nos dois projetos de serviço (tanto ServiceA quanto ServiceB).

Os pacotes que precisam ser instalados são:

  1. Jaeger
  2. OpenTracing
  3. OpenTracing.Contrib.NetCore

Iniciando o Jaeger

Antes de implementar os serviços em ASP.net core, vamos garantir que tenhamos uma instância de Jaeger funcinando.

Para fins de demonstração, iremos utilizar a imagem padrão docker.

docker run -d -p6831:6831/udp -p16686:16686 jaegertracing/all-in-one:latest

Para cenários de produção, precisaríamos configurar o servidor Jaeger.

Implementando ServiceA (cliente)

Basicamente, em ServiceA, precisamos nos preocupar apenas em configurar o tracing e ajustar as chamadas para ServiceB de forma que ela transporte os dados do tracing.

using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

using OpenTracing;
using OpenTracing.Contrib.NetCore.CoreFx;
using OpenTracing.Util;

using Jaeger;
using Jaeger.Samplers;
using Microsoft.Extensions.Logging;


namespace SampleA
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

            services.AddTransient<InjectOpenTracingHeaderHandler>();
            services.AddHttpClient("serviceB", c =>
            {
                c.BaseAddress = new Uri("http://localhost:5003/api/");
            })
            .AddHttpMessageHandler<InjectOpenTracingHeaderHandler>();


            services.AddSingleton<ITracer>(serviceProvider =>
            {
                var serviceName = serviceProvider
                    .GetRequiredService<IHostingEnvironment>()
                    .ApplicationName;

                var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();

                var tracer = new Tracer.Builder(serviceName)
                    .WithSampler(new ConstSampler(true))
                    .WithLoggerFactory(loggerFactory)
                    .Build();

                // Allows code that can't use DI to also access the tracer.
                GlobalTracer.Register(tracer);

                return tracer;
            });

            services.Configure<HttpHandlerDiagnosticOptions>(options =>
            {
                options.IgnorePatterns.Add(x => !x.RequestUri.IsLoopback);
            });
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseMvc();
        }
    }
}

Destaque, no código, para a configuração do HttpClientFactory do cliente para ServiceB. Atente para o fato de que adicionamos um DelegatingHandler no pipe.

using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using OpenTracing;
using OpenTracing.Propagation;
using OpenTracing.Tag;

namespace SampleA
{
    public class InjectOpenTracingHeaderHandler : DelegatingHandler
    {
        private readonly ITracer _tracer;

        public InjectOpenTracingHeaderHandler(ITracer tracer)
        {
            _tracer = tracer;
        }

        protected override Task SendAsync(
            HttpRequestMessage request, 
            CancellationToken cancellationToken
        )
        {
            if (request.Method == HttpMethod.Get)
            {
                var span = _tracer.ScopeManager.Active.Span
                    .SetTag(Tags.SpanKind, Tags.SpanKindClient)
                    .SetTag(Tags.HttpMethod, "GET")
                    .SetTag(Tags.HttpUrl, request.RequestUri.ToString());

                var dictionary = new Dictionary<string, string>();
                _tracer.Inject(span.Context, BuiltinFormats.HttpHeaders, new TextMapInjectAdapter(dictionary));

                foreach (var entry in dictionary)
                    request.Headers.Add(entry.Key, entry.Value);
            }

            return base.SendAsync(request, cancellationToken);
        }
    }
}

Esse handler garante que toda requisição a ServiceB terá os cabeçalhos necessários conforme especificação do OpenTracing.

Abaixo, código do Controller.

using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using OpenTracing;

namespace SampleA.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        private readonly ITracer _tracer;
        private readonly IHttpClientFactory _httpClientFactory;

        public ValuesController(
            ITracer tracer,
            IHttpClientFactory httpClientFactory
            )
        {
            _tracer = tracer;
            _httpClientFactory = httpClientFactory;
        }

        [HttpGet]
        public async Task<ActionResult<IEnumerable>> Get()
        {
            var client = _httpClientFactory.CreateClient("serviceB");

            using (_tracer.BuildSpan("waitingForValues").StartActive(finishSpanOnDispose: true))
            {
                return JsonConvert.DeserializeObject<List>(
                    await client.GetStringAsync("values")
                );
            }
        }
    }
}

A novidade, aqui, foi a delimitação do request em um Span.

Implementando ServiceB (servidor)

Agora que já temos nosso cliente pronto para funcionar, vamos implementar o servidor.

O código de Startup.cs é praticamente idêntico ao que escrevemos para ServiceA, exceto que, dessa vez, não precisamos configurar HttpClientFactory.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

using OpenTracing;
using OpenTracing.Contrib.NetCore.CoreFx;
using OpenTracing.Util;

using Jaeger;
using Jaeger.Samplers;
using Microsoft.Extensions.Logging;

namespace SampleB
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
            
            services.AddSingleton<ITracer>(serviceProvider =>
            {
                var serviceName = serviceProvider
                    .GetRequiredService<IHostingEnvironment>()
                    .ApplicationName;

                var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();

                var tracer = new Tracer.Builder(serviceName)
                    .WithSampler(new ConstSampler(true))
                    .WithLoggerFactory(loggerFactory)
                    .Build();

                // Allows code that can't use DI to also access the tracer.
                GlobalTracer.Register(tracer);

                return tracer;
            });

            services.Configure<HttpHandlerDiagnosticOptions>(options =>
            {
                options.IgnorePatterns.Add(x => !x.RequestUri.IsLoopback);
            });
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseMvc();
        }
    }
}

Além disso, no handler do request, temos que recuperar as informações que está no cabeçalho.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Microsoft.AspNetCore.Mvc;
using OpenTracing;
using OpenTracing.Propagation;
using OpenTracing.Tag;

namespace SampleB.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        private readonly ITracer _tracer;

        public ValuesController(ITracer tracer)
        {
            _tracer = tracer;
        }

        // GET api/values
        [HttpGet]
        public ActionResult<IEnumerable<string>> Get()
        {
            var headers = Request.Headers.ToDictionary(k => k.Key, v => v.Value.First());
            using (var scope = StartServerSpan(_tracer, headers, "producingValues"))
            {
                Thread.Sleep(2000);
                return new[] {"Hello", "OpenTracing!"};
            }
        }

        public static IScope StartServerSpan(ITracer tracer, IDictionary<string, string> headers, string operationName)
        {
            ISpanBuilder spanBuilder;
            try
            {
                var parentSpanCtx = tracer.Extract(BuiltinFormats.HttpHeaders, new TextMapExtractAdapter(headers));

                spanBuilder = tracer.BuildSpan(operationName);
                if (parentSpanCtx != null)
                {
                    spanBuilder = spanBuilder.AsChildOf(parentSpanCtx);
                }
            }
            catch (Exception)
            {
                spanBuilder = tracer.BuildSpan(operationName);
            }

            // TODO could add more tags like http.url
            return spanBuilder.WithTag(Tags.SpanKind, Tags.SpanKindServer).StartActive(true);
        }
    }
}

No exemplo, a função utilitária StartServerSpan (que obtive aqui) recupera os dados de tracing presentes no cabeçalho do request.

Resultado

Nossos serviços, agora, estão devidamente instrumentados e gerando informação de tracing no Jaeger.

O que aprendemos?

Desse exemplo, reconhecemos os seguintes aprendizados:

  1. Há boas implementações de OpenTracing já disponíveis para instrumentar aplicações ASP.net core
  2. HttpClientFactory simplifica a instrumentação de chamadas a serviços remotos permitindo que concentremos o “enriquecimento” dos cabeçalhos em um DelegatingHandler
  3. Recuperar dados das requisições para o contexto de atendimento de um request não é tarefa das mais complexas, podendo ser relegada a uma função utilitária
  4. Distributed Tracing dá uma boa visão do que ocorreu durante o atendimento de um request.

Já usou Distributed Tracing em .NET? O que achou?

Compartilhe este insight:

Comentários

Participe deixando seu comentário sobre este artigo a seguir:

Subscribe
Notify of
guest
1 Comentário
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Roger
Roger
4 anos atrás

Muito bom o post, parabéns!!!

Testei seguindo o exemplo do post e tudo funcionou corretamente.

Fiquei com algumas dúvidas! Caso o serviço B fique indisponível o trace não está sendo guardado, tem como guardar o trace e fazer um log da exception para visualizar no Jaeger?

AUTOR

Elemar Júnior
Fundador e CEO da EximiaCo atua como tech trusted advisor ajudando empresas e profissionais a gerar mais resultados através da tecnologia.

NOVOS HORIZONTES PARA O SEU NEGÓCIO

Nosso time está preparado para superar junto com você grandes desafios tecnológicos.

Entre em contato e vamos juntos utilizar a tecnologia do jeito certo para gerar mais resultados.

Insights EximiaCo

Confira os conteúdos de negócios e tecnologia desenvolvidos pelos nossos consultores:

Arquivo

Pós-pandemia, trabalho remoto e a retenção dos profissionais de TI

CTO Consulting e Especialista em Execução em TI
1
0
Queremos saber a sua opinião, deixe seu comentáriox

A sua inscrição foi realizada com sucesso!

O link de acesso à live foi enviado para o seu e-mail. Nos vemos no dia da live.

Muito obrigado!

Deu tudo certo com seu envio!
Logo entraremos em contato

Usando OpenTracing e Jaeger em serviços distribuídos ASP.net Core

Para se candidatar nesta turma aberta, preencha o formulário a seguir:

Usando OpenTracing e Jaeger em serviços distribuídos ASP.net Core

Para se candidatar nesta turma aberta, preencha o formulário a seguir:

Condição especial de pré-venda: R$ 14.000,00 - contratando a mentoria até até 31/01/2023 e R$ 15.000,00 - contratando a mentoria a partir de 01/02/2023, em até 12x com taxas.

Tenho interesse nessa capacitação

Para solicitar mais informações sobre essa capacitação para a sua empresa, preencha o formulário a seguir:

Tenho interesse em conversar

Se você está querendo gerar resultados através da tecnologia, preencha este formulário que um de nossos consultores entrará em contato com você:

O seu insight foi excluído com sucesso!

O seu insight foi excluído e não está mais disponível.

O seu insight foi salvo com sucesso!

Ele está na fila de espera, aguardando ser revisado para ter sua publicação programada.

Tenho interesse em conversar

Se você está querendo gerar resultados através da tecnologia, preencha este formulário que um de nossos consultores entrará em contato com você:

Tenho interesse nessa solução

Se você está procurando este tipo de solução para o seu negócio, preencha este formulário que um de nossos consultores entrará em contato com você:

Tenho interesse neste serviço

Se você está procurando este tipo de solução para o seu negócio, preencha este formulário que um de nossos consultores entrará em contato com você:

× Precisa de ajuda?