ASP.Net?Core中的日志與分布式鏈路追蹤
程序記錄的日志一般有兩種作用,故障排查、顯式程序運(yùn)行狀態(tài),當(dāng)程序發(fā)生故障時(shí),我們可以通過(guò)日志定位問(wèn)題,日志可以給我們留下排查故障的依據(jù)。很多時(shí)候,往往會(huì)認(rèn)為日志記錄非常簡(jiǎn)單,例如很多程序只是 try-catch{}
,直接輸出到 .txt
,但是這些日志往往無(wú)法起到幫助定位問(wèn)題的作用,甚至日志充斥了大量垃圾內(nèi)容;日志內(nèi)容全靠人眼一行行掃描,或者 Ctrl+F
搜索,無(wú)法高效率審查日志;日志單純輸出到文本文件中,沒(méi)有很好地管理日志。
接下來(lái),我們將一步步學(xué)習(xí)日志的編寫技巧,以及 OpenTracing API 、Jaeger 分布式鏈路跟蹤的相關(guān)知識(shí)。
.NET Core 中的日志
控制臺(tái)輸出
最簡(jiǎn)單的日志,就是控制臺(tái)輸出,利用 Console.WriteLine()
函數(shù)直接輸出信息。
下面時(shí)一個(gè)簡(jiǎn)單的信息輸出,當(dāng)程序調(diào)用 SayHello
函數(shù)時(shí),SayHello
會(huì)打印信息。
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(); } }
非侵入式日志
通過(guò)控制臺(tái),我們可以看到,為了記錄日志,我們必須在函數(shù)內(nèi)編寫輸入日志的代碼,優(yōu)缺點(diǎn)這些就不多說(shuō)了,我們可以通過(guò) AOP 框架,實(shí)現(xiàn)切面編程,同一記錄日志。
這里可以使用筆者開(kāi)源的 CZGL.AOP 框架,Nuget 中可以搜索到。
編寫統(tǒng)一的切入代碼,這些代碼將在函數(shù)被調(diào)用時(shí)執(zhí)行。
Before
會(huì)在被代理的方法執(zhí)行前或被代理的屬性調(diào)用時(shí)生效,你可以通過(guò) AspectContext
上下文,獲取、修改傳遞的參數(shù)。
After 在方法執(zhí)行后或?qū)傩哉{(diào)用時(shí)生效,你可以通過(guò)上下文獲取、修改返回值。
public class LogAttribute : ActionAttribute { public override void Before(AspectContext context) { Console.WriteLine($"{context.MethodInfo.Name} 函數(shù)被執(zhí)行前"); } public override object After(AspectContext context) { Console.WriteLine($"{context.MethodInfo.Name} 函數(shù)被執(zhí)行后"); return null; } }
改造 Hello 類,代碼如下:
[Interceptor] public class Hello { [Log] public virtual void SayHello(string content) { var str = $"Hello,{content}"; Console.WriteLine(str); } }
然后創(chuàng)建代理類型:
static void Main(string[] args) { Hello hello = AopInterceptor.CreateProxyOfClass<Hello>(); hello.SayHello("any one"); Console.Read(); }
啟動(dòng)程序,會(huì)輸出:
SayHello 函數(shù)被執(zhí)行前 Hello,any one SayHello 函數(shù)被執(zhí)行后
你完全不需要擔(dān)心 AOP 框架會(huì)給你的程序帶來(lái)性能問(wèn)題,因?yàn)?CZGL.AOP 框架采用 EMIT 編寫,并且自帶緩存,當(dāng)一個(gè)類型被代理過(guò),之后無(wú)需重復(fù)生成。
CZGL.AOP 可以通過(guò) .NET Core 自帶的依賴注入框架和 Autofac 結(jié)合使用,自動(dòng)代理 CI 容器中的服務(wù)。這樣不需要 AopInterceptor.CreateProxyOfClass
手動(dòng)調(diào)用代理接口。
CZGL.AOP 代碼是開(kāi)源的,可以參考筆者另一篇博文:
http://chabaoo.cn/article/238462.htm
Microsoft.Extensions.Logging
有些公司無(wú)技術(shù)管理規(guī)范,不同的開(kāi)發(fā)人員使用不同的日志框架,一個(gè)產(chǎn)品中可能有 .txt
、NLog
、Serilog
等,并且沒(méi)有同一的封裝。
.NET Core 中的日志組件有很多,但是流行的日志框架基本都會(huì)實(shí)現(xiàn) Microsoft.Extensions.Logging.Abstractions
,因此我們可以學(xué)習(xí)Microsoft.Extensions.Logging
。Microsoft.Extensions.Logging.Abstractions
是官方對(duì)日志組件的抽象,如果一個(gè)日志組件并不支持 Microsoft.Extensions.Logging.Abstractions
那么這個(gè)組件很容易跟項(xiàng)目糅合的,后續(xù)難以模塊化以及降低耦合程度。
Microsoft.Extensions.Logging
軟件包中包含 Logging API ,這些 Logging API 不能獨(dú)立運(yùn)行。它與一個(gè)或多個(gè)日志記錄提供程序一起使用,這些日志記錄提供程序?qū)⑷罩敬鎯?chǔ)或顯示到特定輸出,例如 Console, Debug, TraceListeners。
下圖是 .NET Core 中 Loggin API 的層次結(jié)構(gòu):
說(shuō)實(shí)話,Microsoft.Extensions.Logging
剛開(kāi)始是學(xué)著很懵,配置感覺(jué)很復(fù)雜。因此,有一張清晰的結(jié)構(gòu)圖很重要,可以幫助大家理解里面的 Logging API。
ILoggerFactory
.NET Core 中很多標(biāo)準(zhǔn)接口都實(shí)踐了工廠模式的思想,ILoggerFactory 正是工廠模式的接口,而 LoggerFactory 是工廠模式的實(shí)現(xiàn)。
其定義如下:
public interface ILoggerFactory : IDisposable { ILogger CreateLogger(string categoryName); void AddProvider(ILoggerProvider provider); }
ILoggerFactory 工廠接口的作用是創(chuàng)建一個(gè) ILogger 類型的實(shí)例,即 CreateLogger
接口。
ILoggerProvider
通過(guò)實(shí)現(xiàn)ILoggerProvider
接口可以創(chuàng)建自己的日志記錄提供程序,表示可以創(chuàng)建 ILogger 實(shí)例的類型。
其定義如下:
public interface ILoggerProvider : IDisposable { ILogger CreateLogger(string categoryName); }
ILogger
ILogger 接口提供了將日志記錄到基礎(chǔ)存儲(chǔ)的方法,其定義如下:
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 將日志顯示或存儲(chǔ)到特定介質(zhì),例如 console, debugging event, event log, trace listener 等。
Microsoft.Extensions.Logging
提供了以下類型的 logging providers,我們可以通過(guò) 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等。
這些日志提供程序有很多,我們不必細(xì)究;如果一個(gè)日志組件,不提供兼容 Microsoft.Extensions.Logging
的實(shí)現(xiàn),那么根本不應(yīng)該引入他。
實(shí)際上,很多程序是直接 File.Write("Log.txt")
,這種產(chǎn)品質(zhì)量能好到哪里去呢?
怎么使用
前面,介紹了 Microsoft.Extensions.Logging
的組成,這里將學(xué)習(xí)如何使用 Logging Provider 輸入日志。
起碼提到,它只是提供了一個(gè) Logging API,因此為了輸出日志,我們必須選擇合適的 Logging Provider 程序,這里我們選擇
Microsoft.Extensions.Logging.Console
,請(qǐng)?jiān)?Nuget 中引用這個(gè)包。
下圖是 Logging Provider 和 ConsoleLogger 結(jié)合使用的結(jié)構(gòu)圖:
從常規(guī)方法來(lái)弄,筆者發(fā)現(xiàn),沒(méi)法配置呀。。。
ConsoleLoggerProvider consoleLoggerProvider = new ConsoleLoggerProvider( new OptionsMonitor<ConsoleLoggerOptions>( new OptionsFactory<ConsoleLoggerOptions>( new IEnumerable<IConfigureOptions<TOptions>(... ... ...))));
所以只能使用以下代碼快速創(chuàng)建工廠:
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());
當(dāng)然工廠中可以添加其它日志提供程序,示例:
using ILoggerFactory loggerFactory = LoggerFactory.Create(builder => builder.AddSimpleConsole(...) .AddFile(...) .Add()... );
然后獲取 ILogger 實(shí)例:
ILogger logger = loggerFactory.CreateLogger<Program>();
記錄日志:
logger.LogInformation("記錄信息");
日志等級(jí)
Logging API 中,規(guī)定了 7 種日志等級(jí),其定義如下:
public enum LogLevel { Debug = 1, Verbose = 2, Information = 3, Warning = 4, Error = 5, Critical = 6, None = int.MaxValue }
我們可以通過(guò) ILogger 中的函數(shù),輸出以下幾種等級(jí)的日志:
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.");
關(guān)于 Microsoft.Extensions.Logging
這里就不再贅述。
Trace、Debug
Debug 、Trace 這兩個(gè)類的命名空間為 System.Diagnostics
,Debug 、Trace 提供一組有助于調(diào)試代碼的方法和屬性。
讀者可以參考筆者的另一篇文章:
//chabaoo.cn/article/242562.htm
輸出到控制臺(tái):
Trace.Listeners.Add(new TextWriterTraceListener(Console.Out)); Debug.WriteLine("信息");
鏈路跟蹤
鏈路追蹤可以幫助開(kāi)發(fā)者快速定位分布式應(yīng)用架構(gòu)下的性能瓶頸,提高微服務(wù)時(shí)代的開(kāi)發(fā)診斷效率。
OpenTracing
前面提到的 Trace 、Debug 是 .NET Core 中提供給開(kāi)發(fā)者用于診斷程序和輸出信息的 API,而接著提到的 trace 只 OpenTracing API 中的 鏈路跟蹤(trace)。
普通的日志記錄有很大的缺點(diǎn),就是每個(gè)方法記錄一個(gè)日志,我們無(wú)法將一個(gè)流程中被調(diào)用的多個(gè)方法聯(lián)系起來(lái)。當(dāng)一個(gè)方法出現(xiàn)異常時(shí),我們很難知道是哪個(gè)任務(wù)過(guò)程出現(xiàn)的異常。我們只能看到哪個(gè)方法出現(xiàn)錯(cuò)誤,已經(jīng)它的調(diào)用者。
在 OpenTracing 中,Trace 是具有 Span(跨度) 的有向無(wú)環(huán)圖。一個(gè) Span 代表應(yīng)用程序中完成某些工作的邏輯表示,每個(gè) Span 都具有以下屬性:
- 操作名稱
- 開(kāi)始時(shí)間
- 結(jié)束時(shí)間
為了弄清楚,Trace 和 Span 是什么,OpenTracing 又是什么,請(qǐng)?jiān)?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) { // 創(chuàng)建一個(gè) Span 并開(kāi)始 var spanBuilder = _tracer.BuildSpan("say-hello"); // ------------------------------- var span = spanBuilder.Start(); // | var str = $"Hello,{content}"; // | _logger.LogInformation(str); // | span.Finish(); // | // --------------------------------- } }
啟動(dòng)程序,并開(kāi)始追蹤:
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(); }
在以上過(guò)程中,我們使用了 OpenTracing API,下面是關(guān)于代碼中一些元素的說(shuō)明:
- ITracer 是一個(gè)鏈路追蹤實(shí)例,BuildSpan() 可以創(chuàng)建其中一個(gè) Span;
- 每個(gè) ISpan 都有一個(gè)操作名稱,例如
say-hello
; - 使用
Start()
開(kāi)始一個(gè) Span;使用Finish()
結(jié)束一個(gè) Span; - 跟蹤程序會(huì)自動(dòng)記錄時(shí)間戳;
當(dāng)然,我們運(yùn)行上面的程序時(shí),是沒(méi)有出現(xiàn)別的信息以及 UI 界面,這是因?yàn)?nbsp;GlobalTracer.Instance
會(huì)返回一個(gè)無(wú)操作的 tracer。當(dāng)我們定義一個(gè) Tracer 時(shí),可以觀察到鏈路追蹤的過(guò)程。
在 Nuget 中,引入 Jaeger
。
在 Program 中,添加一個(gè)靜態(tài)函數(shù),這個(gè)函數(shù)返回了一個(gè)自定義的 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 函數(shù)內(nèi)容如下:
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 是很不友好的,這時(shí),我們需要結(jié)構(gòu)化日志。
當(dāng)然,ISpan 提供了結(jié)構(gòu)化日志的方法,我們可以編寫一個(gè)方法,用于格式化日志。
跟蹤單個(gè)功能
在 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(); } }
另外,我們還可以封裝一個(gè)輸出字符串信息的函數(shù):
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(); }
改以上代碼的原因是,不要在一個(gè)方法中糅合太多代碼,可以嘗試將一些代碼復(fù)用,封裝一個(gè)統(tǒng)一的代碼。
但是,原本我們只需要調(diào)用 SayHello 一個(gè)方法,這里一個(gè)方法會(huì)繼續(xù)調(diào)用另外兩個(gè)方法。原本是一個(gè) Span,最后變成三個(gè) 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
表示一個(gè) Span 已經(jīng)結(jié)束。
優(yōu)點(diǎn):從代碼上看,SayHello -> FormaString ,SayHello -> PrintHello,我們可以清晰知道調(diào)用鏈路;
缺點(diǎn):從輸出來(lái)看,Span reported 不同,我們無(wú)法中輸出中判斷三個(gè)函數(shù)的因果關(guān)系;
我們不可能時(shí)時(shí)刻刻都盯著代碼來(lái)看,運(yùn)維人員和實(shí)施人員也不可能拿著代碼去對(duì)比以及查找代碼邏輯。
將多個(gè)跨度合并到一條軌跡中
ITracer 負(fù)責(zé)創(chuàng)建鏈路追蹤,因此 ITracer 也提供了組合多個(gè) Span 因果關(guān)系的 API。
使用方法如下:
var rootSapn = _tracer.BuildSpan("say-hello"); // A var span = _tracer.BuildSpan("format-string").AsChildOf(rootSpan).Start(); // B // A -> B
我們創(chuàng)建了一個(gè) rootSpan ,接著創(chuàng)建一個(gè)延續(xù) rootSpan 的 sapn
,rootSpan -> 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
輸出順序?yàn)閳?zhí)行完畢的順序,say-hello 是最后才執(zhí)行完成的。
傳播過(guò)程中的上下文
從什么代碼中,大家發(fā)現(xiàn),代碼比較麻煩,因?yàn)椋?/p>
- 要將 Span 對(duì)象作為第一個(gè)參數(shù)傳遞給每個(gè)函數(shù);
- 每個(gè)函數(shù)中加上冗長(zhǎng)的
try-finally{}
確保能夠完成 Span
為此, OpenTracing API 提供了一種更好的方法,我們可以避免將 Span 作為參數(shù)傳遞給代碼,可以統(tǒng)一自行調(diào)用 _tracer 即可。
修改 FormatString
和 PrintHello
代碼如下:
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); }
通過(guò)上面的代碼,我們實(shí)現(xiàn)去掉了那些煩人的代碼。
StartActive()
代替Start()
,通過(guò)將其存儲(chǔ)在線程本地存儲(chǔ)中來(lái)使 span 處于“活動(dòng)”狀態(tài);StartActive()
返回一個(gè)IScope
對(duì)象而不是一個(gè)對(duì)象ISpan
。IScope是當(dāng)前活動(dòng)范圍的容器。我們通過(guò)訪問(wèn)活動(dòng)跨度scope.Span
,一旦關(guān)閉了作用域,先前的作用域?qū)⒊蔀楫?dāng)前作用域,從而重新激活當(dāng)前線程中的先前活動(dòng)范圍;IScope
繼承IDisposable
,它使我們可以使用using
語(yǔ)法;StartActive(true)
告訴Scope,一旦它被處理,它就應(yīng)該完成它所代表的范圍;StartActive()
自動(dòng)創(chuàng)建ChildOf
對(duì)先前活動(dòng)范圍的引用,因此我們不必AsChildOf()
顯式使用 builder 方法;
如果運(yùn)行此程序,我們將看到所有三個(gè)報(bào)告的跨度都具有相同的跟蹤ID。
分布式鏈路跟蹤
在不同進(jìn)程中跟蹤
微服務(wù)將多個(gè)程序分開(kāi)部署,每個(gè)程序提供不同的功能。在前面,我們已經(jīng)學(xué)會(huì)了 OpenTracing 鏈路跟蹤。接下來(lái),我們將把代碼拆分,控制臺(tái)程序?qū)⒉辉偬峁?FormatString 函數(shù)的實(shí)現(xiàn),我們使用 一個(gè) Web 程序來(lái)實(shí)現(xiàn) FormatString 服務(wù)。
創(chuàng)建一個(gè) ASP.NET Core 應(yīng)用程序,在模板中選擇帶有視圖模型控制器的模板。
添加一個(gè) 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 應(yīng)用將作為微服務(wù)中的其中一個(gè)服務(wù),而這個(gè)服務(wù)只有一個(gè) API ,這個(gè) API 很簡(jiǎn)單,就是提供字符串的格式化。你也可以編寫其它 API 來(lái)提供服務(wù)。
將 Program 的 CreateHostBuilder 改一下,我們固定這個(gè)服務(wù)的 端口。
public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseUrls("http://*:8081"); webBuilder.UseStartup<Startup>(); });
再到 Startup
中刪除 app.UseHttpsRedirection();
。
修改之前控制臺(tái)程序的代碼,把 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; } }
啟動(dòng) Web 程序后,再啟動(dòng) 控制臺(tái)程序。
控制臺(tái)程序輸出:
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
可以設(shè)置標(biāo)簽,我們?yōu)楸敬握?qǐng)求到 Web 的 Span,設(shè)置一個(gè)標(biāo)簽,并且存儲(chǔ)請(qǐng)求的 URL。
var span = scope.Span .SetTag(Tags.SpanKind, Tags.SpanKindClient) .SetTag(Tags.HttpMethod, "GET") .SetTag(Tags.HttpUrl, url);
通過(guò) Inject
將上下文信息注入。
_tracer.Inject(span.Context, BuiltinFormats.HttpHeaders, new TextMapInjectAdapter(dictionary));
這些配置規(guī)范,可以到 https://github.com/opentracing/specification/blob/master/semantic_conventions.md 了解。
在 ASP.NET Core 中跟蹤
在上面,我們實(shí)現(xiàn)了 Client 在不同進(jìn)程的追蹤,但是還沒(méi)有實(shí)現(xiàn)在 Server 中跟蹤,我們可以修改 Startup.cs 中的代碼,將以下代碼替換進(jìn)去:
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?}"); }); } } }
這樣不同的進(jìn)程各種都可以實(shí)現(xiàn)追蹤。
OpenTracing API 和 Jaeger
OpenTracing 是開(kāi)放式分布式追蹤規(guī)范,OpenTracing API 是一致,可表達(dá),與供應(yīng)商無(wú)關(guān)的API,用于分布式跟蹤和上下文傳播。
Jaeger 是 Uber 開(kāi)源的分布式跟蹤系統(tǒng)。
OpenTracing 的客戶端庫(kù)以及規(guī)范,可以到 Github 中查看:https://github.com/opentracing/
詳細(xì)的介紹可以自行查閱資料。
這里我們需要部署一個(gè) Jaeger 實(shí)例,以供微服務(wù)以及事務(wù)跟蹤學(xué)習(xí)需要。
使用 Docker 部署很簡(jiǎn)單,只需要執(zhí)行下面一條命令即可:
docker run -d -p 5775:5775/udp -p 16686:16686 -p 14250:14250 -p 14268:14268 jaegertracing/all-in-one:latest
訪問(wèn) 16686 端口,即可看到 UI 界面。
Jaeger 的端口作用如下:
Collector 14250 tcp gRPC 發(fā)送 proto 格式數(shù)據(jù) 14268 http 直接接受客戶端數(shù)據(jù) 14269 http 健康檢查 Query 16686 http jaeger的UI前端 16687 http 健康檢查
接下來(lái)我們將學(xué)習(xí)如何通過(guò)代碼,將數(shù)據(jù)上傳到 Jaeger 中。
鏈路追蹤實(shí)踐
要注意,數(shù)據(jù)上傳到 Jaeger ,上傳的是 Span,是不會(huì)上傳日志內(nèi)容的。
繼續(xù)使用上面的控制臺(tái)程序,Nuget 中添加 Jaeger.Senders.Grpc
包。
我們可以通過(guò) UDP (6831端口)和 gRPC(14250) 端口將數(shù)據(jù)上傳到 Jaeger 中,這里我們使用 gRPC。
修改控制臺(tái)程序的 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(); }
分別啟動(dòng) Web 和 控制臺(tái)程序,然后打開(kāi) Jaeger 界面,在 ”Service“ 中選擇 hello-world
,然后點(diǎn)擊底下的 Find Traces
。
通過(guò) Jaeger ,我們可以分析鏈路中函數(shù)的執(zhí)行速度以及服務(wù)器性能情況。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
ASP.NET實(shí)現(xiàn)偽靜態(tài)網(wǎng)頁(yè)方法小結(jié)
這篇文章主要介紹了ASP.NET實(shí)現(xiàn)偽靜態(tài)網(wǎng)頁(yè)方法小結(jié),主要包括了利用Httphandler實(shí)現(xiàn)URL重寫、地址重寫、利用Mircosoft URLRewriter.dll實(shí)現(xiàn)頁(yè)面?zhèn)戊o態(tài)等,需要的朋友可以參考下2014-09-09WebForm獲取checkbox選中的值(幾個(gè)簡(jiǎn)單的示例)
WebForm中用checkbox的地方挺多的,下面寫了幾個(gè)簡(jiǎn)單的例子,方便以后學(xué)習(xí)使用2014-07-07ASP.NET MVC學(xué)習(xí)教程之Razor語(yǔ)法
這篇文章主要給大家介紹了關(guān)于ASP.NET MVC學(xué)習(xí)教程之Razor語(yǔ)法的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-05-05在.Net?Framework應(yīng)用中請(qǐng)求HTTP2站點(diǎn)的問(wèn)題解析
隨著各大瀏覽器支持和蘋果的帶頭效應(yīng),HTTP2的應(yīng)用會(huì)越來(lái)越廣泛,但是規(guī)模龐大的.NET?Framework應(yīng)用卻也不能為了連接HTTP2就升級(jí)到NET?Core平臺(tái)。通過(guò)本文提供的方案,可以最小成本的實(shí)現(xiàn).NET?Framework應(yīng)用成功訪問(wèn)HTTP2站點(diǎn),感興趣的朋友跟隨小編一起看看吧2022-07-07JWT + ASP.NET MVC時(shí)間戳防止重放攻擊詳解
這篇文章主要給大家介紹了關(guān)于JWT + ASP.NET MVC時(shí)間戳防止重放攻擊發(fā)的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-07-07ASP.NET?Core基于滑動(dòng)窗口實(shí)現(xiàn)限流控制
這篇文章主要介紹了ASP.NET?Core基于滑動(dòng)窗口實(shí)現(xiàn)限流控制,?AspNetCoreRateLimit是目前ASP.NET?Core下最常用的限流解決方案,下面詳細(xì)內(nèi)容,需要的小伙伴可以參考一下2022-03-03