.NET Core 中的日誌與分散式鏈路追蹤

微軟技術棧發表於2021-01-09

.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 ,我們可以分析鏈路中函式的執行速度以及伺服器效能情況。

相關文章