.NET Core 中的日志与分布式链路追踪

内容纲要

.NET Core 中的日志与分布式链路追踪

程序记录的日志一般有两种作用,故障排查、显式程序运行状态,当程序发生故障时,我们可以通过日志定位问题,日志可以给我们留下排查故障的依据。很多时候,往往会认为日志记录非常简单,例如很多程序只是 try-catch{},直接输出到 .txt,但是这些日志往往无法起到帮助定位问题的作用,甚至日志充斥了大量垃圾内容;日志内容全靠人眼一行行扫描,或者 Ctrl+F 搜索,无法高效率审查日志;日志单纯输出到文本文件中,没有很好地管理日志。

接下来,我们将一步步学习日志的编写技巧,以及 OpenTracing API 、Jaeger 分布式链路跟踪的相关知识。

.NET Core 中的日志

控制台输出

最简单的日志,就是控制台输出,利用 Console.WriteLine() 函数直接输出信息。

下面时一个简单的信息输出,当程序调用 SayHello 函数时,SayHello 会打印信息。

    public class Hello
    {
        public void SayHello(string content)
        {
            var str = $"Hello,{content}";
            Console.WriteLine(str);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Hello hello = new Hello();
            hello.SayHello("any one");
            Console.Read();
        }
    }

非侵入式日志

通过控制台,我们可以看到,为了记录日志,我们必须在函数内编写输入日志的代码,优缺点这些就不多说了,我们可以通过 AOP 框架,实现切面编程,同一记录日志。

这里可以使用笔者开源的 CZGL.AOP 框架,Nuget 中可以搜索到。

《.NET Core 中的日志与分布式链路追踪》

编写统一的切入代码,这些代码将在函数被调用时执行。

Before 会在被代理的方法执行前或被代理的属性调用时生效,你可以通过 AspectContext 上下文,获取、修改传递的参数。

After 在方法执行后或属性调用时生效,你可以通过上下文获取、修改返回值。

    public class LogAttribute : ActionAttribute
    {
        public override void Before(AspectContext context)
        {
            Console.WriteLine($"{context.MethodInfo.Name} 函数被执行前");
        }

        public override object After(AspectContext context)
        {
            Console.WriteLine($"{context.MethodInfo.Name} 函数被执行后");
            return null;
        }
    }

改造 Hello 类,代码如下:

    [Interceptor]
    public class Hello
    {
        [Log]
        public virtual void SayHello(string content)
        {
            var str = $"Hello,{content}";
            Console.WriteLine(str);
        }
    }

然后创建代理类型:

        static void Main(string[] args)
        {
            Hello hello = AopInterceptor.CreateProxyOfClass<Hello>();
            hello.SayHello("any one");
            Console.Read();
        }

启动程序,会输出:

SayHello 函数被执行前
Hello,any one
SayHello 函数被执行后

你完全不需要担心 AOP 框架会给你的程序带来性能问题,因为 CZGL.AOP 框架采用 EMIT 编写,并且自带缓存,当一个类型被代理过,之后无需重复生成。

CZGL.AOP 可以通过 .NET Core 自带的依赖注入框架和 Autofac 结合使用,自动代理 CI 容器中的服务。这样不需要 AopInterceptor.CreateProxyOfClass 手动调用代理接口。

CZGL.AOP 代码是开源的,可以参考笔者另一篇博文:

https://www.cnblogs.com/whuanle/p/13160139.html

Microsoft.Extensions.Logging

有些公司无技术管理规范,不同的开发人员使用不同的日志框架,一个产品中可能有 .txtNLogSerilog等,并且没有同一的封装。

.NET Core 中的日志组件有很多,但是流行的日志框架基本都会实现 Microsoft.Extensions.Logging.Abstractions,因此我们可以学习Microsoft.Extensions.LoggingMicrosoft.Extensions.Logging.Abstractions 是官方对日志组件的抽象,如果一个日志组件并不支持 Microsoft.Extensions.Logging.Abstractions 那么这个组件很容易跟项目糅合的,后续难以模块化以及降低耦合程度。

Microsoft.Extensions.Logging 软件包中包含 Logging API ,这些 Logging API 不能独立运行。它与一个或多个日志记录提供程序一起使用,这些日志记录提供程序将日志存储或显示到特定输出,例如 Console, Debug, TraceListeners。

下图是 .NET Core 中 Loggin API 的层次结构:

图片来源:https://www.tutorialsteacher.com/

《.NET Core 中的日志与分布式链路追踪》

说实话,Microsoft.Extensions.Logging 刚开始是学着很懵,配置感觉很复杂。因此,有一张清晰的结构图很重要,可以帮助大家理解里面的 Logging API。

《.NET Core 中的日志与分布式链路追踪》

ILoggerFactory

.NET Core 中很多标准接口都实践了工厂模式的思想,ILoggerFactory 正是工厂模式的接口,而 LoggerFactory 是工厂模式的实现。

其定义如下:

public interface ILoggerFactory : IDisposable
{
    ILogger CreateLogger(string categoryName);
    void AddProvider(ILoggerProvider provider);
}

ILoggerFactory 工厂接口的作用是创建一个 ILogger 类型的实例,即 CreateLogger 接口。

ILoggerProvider

通过实现ILoggerProvider接口可以创建自己的日志记录提供程序,表示可以创建 ILogger 实例的类型。

其定义如下:

public interface ILoggerProvider : IDisposable
{
    ILogger CreateLogger(string categoryName);
}

ILogger

ILogger 接口提供了将日志记录到基础存储的方法,其定义如下:

public interface ILogger
{
    void Log<TState>(LogLevel logLevel, 
                     EventId eventId, 
                     TState state, 
                     Exception exception, 
                     Func<TState, Exception, string> formatter);

    bool IsEnabled(LogLevel logLevel);
    IDisposable BeginScope<TState>(TState state);
} 

Logging Providers

logging providers 称为日志记录程序。

Logging Providers 将日志显示或存储到特定介质,例如 console, debugging event, event log, trace listener 等。

Microsoft.Extensions.Logging 提供了以下类型的 logging providers,我们可以通过 Nuget 获取。

  • Microsoft.Extensions.Logging.Console
  • Microsoft.Extensions.Logging.AzureAppServices
  • Microsoft.Extensions.Logging.Debug
  • Microsoft.Extensions.Logging.EventLog
  • Microsoft.Extensions.Logging.EventSource
  • Microsoft.Extensions.Logging.TraceSource

而 Serilog 则有 File、Console、Elasticsearch、Debug、MSSqlServer、Email等。

这些日志提供程序有很多,我们不必细究;如果一个日志组件,不提供兼容 Microsoft.Extensions.Logging 的实现,那么根本不应该引入他。

实际上,很多程序是直接 File.Write("Log.txt") ,这种产品质量能好到哪里去呢?

怎么使用

前面,介绍了 Microsoft.Extensions.Logging 的组成,这里将学习如何使用 Logging Provider 输入日志。

起码提到,它只是提供了一个 Logging API,因此为了输出日志,我们必须选择合适的 Logging Provider 程序,这里我们选择

Microsoft.Extensions.Logging.Console,请在 Nuget 中引用这个包。

下图是 Logging Provider 和 ConsoleLogger 结合使用的结构图:

《.NET Core 中的日志与分布式链路追踪》

从常规方法来弄,笔者发现,没法配置呀。。。

            ConsoleLoggerProvider consoleLoggerProvider = new ConsoleLoggerProvider(
                new OptionsMonitor<ConsoleLoggerOptions>(
                    new OptionsFactory<ConsoleLoggerOptions>(
                        new IEnumerable<IConfigureOptions<TOptions>(... ... ...))));

所以只能使用以下代码快速创建工厂:

            using ILoggerFactory loggerFactory =
                LoggerFactory.Create(builder =>
                    builder.AddSimpleConsole(options =>
                    {
                        options.IncludeScopes = true;
                        options.SingleLine = true;
                        options.TimestampFormat = "hh:mm:ss ";
                    }));

或者:

ILoggerFactory loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());

当然工厂中可以添加其它日志提供程序,示例:

            using ILoggerFactory loggerFactory =
                LoggerFactory.Create(builder =>
                    builder.AddSimpleConsole(...)
                    .AddFile(...)
                    .Add()...
                    );

然后获取 ILogger 实例:

  ILogger logger = loggerFactory.CreateLogger<Program>();

记录日志:

            logger.LogInformation("记录信息");

日志等级

Logging API 中,规定了 7 种日志等级,其定义如下:

public enum LogLevel
{
  Debug = 1,
  Verbose = 2,
  Information = 3,
  Warning = 4,
  Error = 5,
  Critical = 6,
  None = int.MaxValue
}

我们可以通过 ILogger 中的函数,输出以下几种等级的日志:

            logger.LogInformation("Logging information.");
            logger.LogCritical("Logging critical information.");
            logger.LogDebug("Logging debug information.");
            logger.LogError("Logging error information.");
            logger.LogTrace("Logging trace");
            logger.LogWarning("Logging warning.");

关于 Microsoft.Extensions.Logging 这里就不再赘述,读者可以等级以下链接,了解更多相关知识:

https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/logging/?view=aspnetcore-5.0#log-exceptions

https://www.tutorialsteacher.com/core/fundamentals-of-logging-in-dotnet-core

https://docs.microsoft.com/en-us/archive/msdn-magazine/2016/april/essential-net-logging-with-net-core

Trace、Debug

Debug 、Trace 这两个类的命名空间为 System.Diagnostics,Debug 、Trace 提供一组有助于调试代码的方法和属性。

读者可以参考笔者的另一篇文章:

https://www.cnblogs.com/whuanle/p/14141213.html#3

输出到控制台:

Trace.Listeners.Add(new TextWriterTraceListener(Console.Out));
Debug.WriteLine("信息");

链路跟踪

链路追踪可以帮助开发者快速定位分布式应用架构下的性能瓶颈,提高微服务时代的开发诊断效率。

OpenTracing

前面提到的 Trace 、Debug 是 .NET Core 中提供给开发者用于诊断程序和输出信息的 API,而接着提到的 trace 只 OpenTracing API 中的 链路跟踪(trace)。

普通的日志记录有很大的缺点,就是每个方法记录一个日志,我们无法将一个流程中被调用的多个方法联系起来。当一个方法出现异常时,我们很难知道是哪个任务过程出现的异常。我们只能看到哪个方法出现错误,已经它的调用者。

在 OpenTracing 中,Trace 是具有 Span(跨度) 的有向无环图。一个 Span 代表应用程序中完成某些工作的逻辑表示,每个 Span 都具有以下属性:

  • 操作名称
  • 开始时间
  • 结束时间

为了弄清楚,Trace 和 Span 是什么,OpenTracing 又是什么,请在 Nuget 中引入 OpenTracing

编写 Hello 类如下:

    public class Hello
    {
        private readonly ITracer _tracer;
        private readonly ILogger<Hello> _logger;
        public Hello(ITracer tracer, ILoggerFactory loggerFactory)
        {
            _tracer = tracer;
            _logger = loggerFactory.CreateLogger<Hello>();
        }

        public void SayHello(string content)
        {
            // 创建一个 Span 并开始
            var spanBuilder = _tracer.BuildSpan("say-hello");
            // -------------------------------
            var span = spanBuilder.Start(); // |
            var str = $"Hello,{content}";   // |
            _logger.LogInformation(str);    // |
            span.Finish();                  // |
            // ---------------------------------
        }
    }

启动程序,并开始追踪:

        static void Main(string[] args)
        {
            using ILoggerFactory loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());

            Hello hello = new Hello(GlobalTracer.Instance, loggerFactory);
            hello.SayHello("This trace");
            Console.Read();
        }

在以上过程中,我们使用了 OpenTracing API,下面是关于代码中一些元素的说明:

  • ITracer 是一个链路追踪实例,BuildSpan() 可以创建其中一个 Span;
  • 每个 ISpan 都有一个操作名称,例如 say-hello
  • 使用 Start() 开始一个 Span;使用 Finish() 结束一个 Span;
  • 跟踪程序会自动记录时间戳;

当然,我们运行上面的程序时,是没有出现别的信息以及 UI 界面,这是因为 GlobalTracer.Instance 会返回一个无操作的 tracer。当我们定义一个 Tracer 时,可以观察到链路追踪的过程。

在 Nuget 中,引入 Jaeger

在 Program 中,添加一个静态函数,这个函数返回了一个自定义的 Tracer:

private static Tracer InitTracer(string serviceName, ILoggerFactory loggerFactory)
{
    var samplerConfiguration = new Configuration.SamplerConfiguration(loggerFactory)
        .WithType(ConstSampler.Type)
        .WithParam(1);

    var reporterConfiguration = new Configuration.ReporterConfiguration(loggerFactory)
        .WithLogSpans(true);

    return (Tracer)new Configuration(serviceName, loggerFactory)
        .WithSampler(samplerConfiguration)
        .WithReporter(reporterConfiguration)
        .GetTracer();
}

修改 Main 函数内容如下:

        static void Main(string[] args)
        {
            using ILoggerFactory loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
            var tracer = InitTracer("hello-world", loggerFactory);
            Hello hello = new Hello(tracer, loggerFactory);
            hello.SayHello("This trace");
            Console.Read();
        }

完整代码:https://gist.github.com/whuanle/b57fe79c9996988db0a9b812f403f00e

上下文和跟踪功能

但是,日志直接输出 string 是很不友好的,这时,我们需要结构化日志。

当然,ISpan 提供了结构化日志的方法,我们可以编写一个方法,用于格式化日志。

跟踪单个功能

在 Hello 类中添加以下代码:

private string FormatString(ISpan rootSpan, string helloTo)
{
    var span = _tracer.BuildSpan("format-string").Start();
    try
    {
        var helloString = $"Hello, {helloTo}!";
        span.Log(new Dictionary<string, object>
        {
            [LogFields.Event] = "string.Format",
            ["value"] = helloString
        });
        return helloString;
    }
    finally
    {
        span.Finish();
    }
}

另外,我们还可以封装一个输出字符串信息的函数:

private void PrintHello(ISpan rootSpan, string helloString)
{
    var span = _tracer.BuildSpan("print-hello").Start();
    try
    {
        _logger.LogInformation(helloString);
        span.Log("WriteLine");
    }
    finally
    {
        span.Finish();
    }
}

将 SayHello 方法改成:

        public void SayHello(string content)
        {
            var spanBuilder = _tracer.BuildSpan("say-hello");
            var span = spanBuilder.Start();
            var str = FormatString(span, content);
            PrintHello(span,str);
            span.Finish();
        }

改以上代码的原因是,不要在一个方法中糅合太多代码,可以尝试将一些代码复用,封装一个统一的代码。

但是,原本我们只需要调用 SayHello 一个方法,这里一个方法会继续调用另外两个方法。原本是一个 Span,最后变成三个 Span。

info: Jaeger.Configuration[0]
info: Jaeger.Reporters.LoggingReporter[0]
      Span reported: 77f1a24676a3ffe1:77f1a24676a3ffe1:0000000000000000:1 - format-string
info: ConsoleApp1.Hello[0]
      Hello, This trace!
info: Jaeger.Reporters.LoggingReporter[0]
      Span reported: cebd31b028a27882:cebd31b028a27882:0000000000000000:1 - print-hello
info: Jaeger.Reporters.LoggingReporter[0]
      Span reported: 44d89e11c8ef51d6:44d89e11c8ef51d6:0000000000000000:1 - say-hello

注:0000000000000000 表示一个 Span 已经结束。

优点:从代码上看,SayHello -> FormaString ,SayHello -> PrintHello,我们可以清晰知道调用链路;

缺点:从输出来看,Span reported 不同,我们无法中输出中判断三个函数的因果关系;

我们不可能时时刻刻都盯着代码来看,运维人员和实施人员也不可能拿着代码去对比以及查找代码逻辑。

将多个跨度合并到一条轨迹中

ITracer 负责创建链路追踪,因此 ITracer 也提供了组合多个 Span 因果关系的 API。

使用方法如下:

var rootSapn = _tracer.BuildSpan("say-hello");  // A
var span = _tracer.BuildSpan("format-string").AsChildOf(rootSpan).Start();    // B
// A -> B

我们创建了一个 rootSpan ,接着创建一个延续 rootSpan 的 sapnrootSpan -> span

info: Jaeger.Reporters.LoggingReporter[0]
      Span reported: 2f2c7b36f4f6b0b9:3dab62151c641380:2f2c7b36f4f6b0b9:1 - format-string
info: ConsoleApp1.Hello[0]
      Hello, This trace!
info: Jaeger.Reporters.LoggingReporter[0]
      Span reported: 2f2c7b36f4f6b0b9:9824227a41539786:2f2c7b36f4f6b0b9:1 - print-hello
info: Jaeger.Reporters.LoggingReporter[0]
      Span reported: 2f2c7b36f4f6b0b9:2f2c7b36f4f6b0b9:0000000000000000:1 - say-hello
Span reported: 2f2c7b36f4f6b0b9

输出顺序为执行完毕的顺序,say-hello 是最后才执行完成的。

传播过程中的上下文

从什么代码中,大家发现,代码比较麻烦,因为:

  • 要将 Span 对象作为第一个参数传递给每个函数;
  • 每个函数中加上冗长的 try-finally{} 确保能够完成 Span

为此, OpenTracing API 提供了一种更好的方法,我们可以避免将 Span 作为参数传递给代码,可以统一自行调用 _tracer 即可。

修改 FormatStringPrintHello 代码如下:

    private string FormatString(string helloTo)
    {
        using var scope = _tracer.BuildSpan("format-string").StartActive(true);
        var helloString = $"Hello, {helloTo}!";
        scope.Span.Log(new Dictionary<string, object>
        {
            [LogFields.Event] = "string.Format",
            ["value"] = helloString
        });
        return helloString;
    }

    private void PrintHello(string helloString)
    {
        using var scope = _tracer.BuildSpan("print-hello").StartActive(true);
        _logger.LogInformation(helloString);
        scope.Span.Log(new Dictionary<string, object>
        {
            [LogFields.Event] = "WriteLine"
        });
    }

修改 SayHello 代码如下:

public void SayHello(string helloTo)
{
            using var scope = _tracer.BuildSpan("say-hello").StartActive(true);
            scope.Span.SetTag("hello-to", helloTo);
            var helloString = FormatString(helloTo);
            PrintHello(helloString);
}

通过上面的代码,我们实现去掉了那些烦人的代码。

  • StartActive() 代替Start(),通过将其存储在线程本地存储中来使 span 处于“活动”状态;
  • StartActive() 返回一个IScope对象而不是一个对象ISpan。IScope是当前活动范围的容器。我们通过访问活动跨度scope.Span,一旦关闭了作用域,先前的作用域将成为当前作用域,从而重新激活当前线程中的先前活动范围;
  • IScope 继承 IDisposable,它使我们可以使用using语法;
  • StartActive(true)告诉Scope,一旦它被处理,它就应该完成它所代表的范围;
  • StartActive()自动创建 ChildOf 对先前活动范围的引用,因此我们不必AsChildOf()显式使用 builder 方法;

如果运行此程序,我们将看到所有三个报告的跨度都具有相同的跟踪ID。

分布式链路跟踪

在不同进程中跟踪

微服务将多个程序分开部署,每个程序提供不同的功能。在前面,我们已经学会了 OpenTracing 链路跟踪。接下来,我们将把代码拆分,控制台程序将不再提供 FormatString 函数的实现,我们使用 一个 Web 程序来实现 FormatString 服务。

创建一个 ASP.NET Core 应用程序,在模板中选择带有视图模型控制器的模板。

添加一个 FormatController 控制器在 Controllers 目录中,其代码如下:

using Microsoft.AspNetCore.Mvc;

namespace WebApplication1.Controllers
{
    [Route("api/[controller]")]
    public class FormatController : Controller
    {
        [HttpGet]
        public string Get()
        {
            return "Hello!";
        }

        [HttpGet("{helloTo}", Name = "GetFormat")]
        public string Get(string helloTo)
        {
            var formattedHelloString = $"Hello, {helloTo}!";
            return formattedHelloString;
        }
    }
}

Web 应用将作为微服务中的其中一个服务,而这个服务只有一个 API ,这个 API 很简单,就是提供字符串的格式化。你也可以编写其它 API 来提供服务。

将 Program 的 CreateHostBuilder 改一下,我们固定这个服务的 端口。

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseUrls("http://*:8081");
                    webBuilder.UseStartup<Startup>();
                });

再到 Startup 中删除 app.UseHttpsRedirection();

修改之前控制台程序的代码,把 FormatString 方法改成:

        private string FormatString(string helloTo)
        {
            using (var scope = _tracer.BuildSpan("format-string").StartActive(true))
            {
                using WebClient webClient = new WebClient();
                var url = $"http://localhost:8081/api/format/{helloTo}";
                var helloString = webClient.DownloadString(url);
                scope.Span.Log(new Dictionary<string, object>
                {
                    [LogFields.Event] = "string.Format",
                    ["value"] = helloString
                });
                return helloString;
            }
        }

启动 Web 程序后,再启动 控制台程序。

控制台程序输出:

info: Jaeger.Reporters.LoggingReporter[0]
      Span reported: c587bd888e8f1c19:2e3273568e6e373b:c587bd888e8f1c19:1 - format-string
info: ConsoleApp1.Hello[0]
      Hello, This trace!
info: Jaeger.Reporters.LoggingReporter[0]
      Span reported: c587bd888e8f1c19:f0416a0130d58924:c587bd888e8f1c19:1 - print-hello
info: Jaeger.Reporters.LoggingReporter[0]
      Span reported: c587bd888e8f1c19:c587bd888e8f1c19:0000000000000000:1 - say-hello

接着,我们可以将 Formating 改成:

        private string FormatString(string helloTo)
        {
            using (var scope = _tracer.BuildSpan("format-string").StartActive(true))
            {
                using WebClient webClient = new WebClient();
                var url = $"http://localhost:8081/api/format/{helloTo}";
                var helloString = webClient.DownloadString(url);
                var span = scope.Span
                    .SetTag(Tags.SpanKind, Tags.SpanKindClient)
                    .SetTag(Tags.HttpMethod, "GET")
                    .SetTag(Tags.HttpUrl, url);

                var dictionary = new Dictionary<string, string>();
                _tracer.Inject(span.Context, BuiltinFormats.HttpHeaders, new TextMapInjectAdapter(dictionary));
                foreach (var entry in dictionary)
                    webClient.Headers.Add(entry.Key, entry.Value);
                return helloString;
            }
        }

SetTag 可以设置标签,我们为本次请求到 Web 的 Span,设置一个标签,并且存储请求的 URL。

                var span = scope.Span
                    .SetTag(Tags.SpanKind, Tags.SpanKindClient)
                    .SetTag(Tags.HttpMethod, "GET")
                    .SetTag(Tags.HttpUrl, url);

通过 Inject 将上下文信息注入。

                _tracer.Inject(span.Context, BuiltinFormats.HttpHeaders, new TextMapInjectAdapter(dictionary));

这些配置规范,可以到 https://github.com/opentracing/specification/blob/master/semantic_conventions.md 了解。

在 ASP.NET Core 中跟踪

在上面,我们实现了 Client 在不同进程的追踪,但是还没有实现在 Server 中跟踪,我们可以修改 Startup.cs 中的代码,将以下代码替换进去:

using Jaeger;
using Jaeger.Samplers;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using OpenTracing.Util;
using System;

namespace WebApplication1
{
    public class Startup
    {
        private static readonly ILoggerFactory loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
        private static readonly Lazy<Tracer> Tracer = new Lazy<Tracer>(() =>
        {
            return InitTracer("webService", loggerFactory);
        });
        private static Tracer InitTracer(string serviceName, ILoggerFactory loggerFactory)
        {
            var samplerConfiguration = new Configuration.SamplerConfiguration(loggerFactory)
                .WithType(ConstSampler.Type)
                .WithParam(1);

            var reporterConfiguration = new Configuration.ReporterConfiguration(loggerFactory)
                .WithLogSpans(true);

            return (Tracer)new Configuration(serviceName, loggerFactory)
                .WithSampler(samplerConfiguration)
                .WithReporter(reporterConfiguration)
                .GetTracer();
        }
        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();

            GlobalTracer.Register(Tracer.Value);
            services.AddOpenTracing();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app)
        {
            app.UseRouting();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

这样不同的进程各种都可以实现追踪。

OpenTracing API 和 Jaeger

OpenTracing 是开放式分布式追踪规范,OpenTracing API 是一致,可表达,与供应商无关的API,用于分布式跟踪和上下文传播。

Jaeger 是 Uber 开源的分布式跟踪系统。

OpenTracing 的客户端库以及规范,可以到 Github 中查看:https://github.com/opentracing/

详细的介绍可以自行查阅资料。

这里我们需要部署一个 Jaeger 实例,以供微服务以及事务跟踪学习需要。

使用 Docker 部署很简单,只需要执行下面一条命令即可:

docker run -d -p 5775:5775/udp -p 16686:16686 -p 14250:14250 -p 14268:14268 jaegertracing/all-in-one:latest

访问 16686 端口,即可看到 UI 界面。

《.NET Core 中的日志与分布式链路追踪》

Jaeger 的端口作用如下:

Collector
14250 tcp  gRPC 发送 proto 格式数据
14268 http 直接接受客户端数据
14269 http 健康检查
Query
16686 http jaeger的UI前端
16687 http 健康检查

接下来我们将学习如何通过代码,将数据上传到 Jaeger 中。

链路追踪实践

要注意,数据上传到 Jaeger ,上传的是 Span,是不会上传日志内容的。

继续使用上面的控制台程序,Nuget 中添加 Jaeger.Senders.Grpc 包。

我们可以通过 UDP (6831端口)和 gRPC(14250) 端口将数据上传到 Jaeger 中,这里我们使用 gRPC。

修改控制台程序的 InitTracer 方法,其代码如下:

        private static Tracer InitTracer(string serviceName, ILoggerFactory loggerFactory)
        {
            Configuration.SenderConfiguration.DefaultSenderResolver = new SenderResolver(loggerFactory)
                .RegisterSenderFactory<GrpcSenderFactory>();

            var reporter = new RemoteReporter.Builder()
                .WithLoggerFactory(loggerFactory)
                .WithSender(new GrpcSender("180.102.130.181:14250", null, 0))
                .Build();

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

            return tracer.Build();
        }

分别启动 Web 和 控制台程序,然后打开 Jaeger 界面,在 ”Service“ 中选择 hello-world,然后点击底下的 Find Traces

《.NET Core 中的日志与分布式链路追踪》

《.NET Core 中的日志与分布式链路追踪》

通过 Jaeger ,我们可以分析链路中函数的执行速度以及服务器性能情况。

点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注