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:
- Jaeger
- OpenTracing
- 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:
- Há boas implementações de OpenTracing já disponíveis para instrumentar aplicações ASP.net core
- HttpClientFactory simplifica a instrumentação de chamadas a serviços remotos permitindo que concentremos o “enriquecimento” dos cabeçalhos em um DelegatingHandler
- 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
- 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?
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?