.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 中可以搜索到。

czgl.aop

編寫統一的切入代碼,這些代碼將在函數被調用時執行。

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/

logginapi

說實話,Microsoft.Extensions.Logging 剛開始是學着很懵,配置感覺很復雜。因此,有一張清晰的結構圖很重要,可以幫助大家理解里面的 Logging API。

logging-api

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 結合使用的結構圖:

console-logger

從常規方法來弄,筆者發現,沒法配置呀。。。

            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 界面。

JaegerUI

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

search

hello-world

通過 Jaeger ,我們可以分析鏈路中函數的執行速度以及服務器性能情況。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM