[{"data":1,"prerenderedAt":711},["ShallowReactive",2],{"article-\u002Farticles\u002Fopentelemetry-aspnetcore-instrumentation":3},{"id":4,"title":5,"body":6,"date":698,"description":699,"excerpt":700,"extension":701,"meta":702,"navigation":211,"path":703,"seo":704,"stem":705,"tags":706,"__hash__":710},"articles\u002Farticles\u002Fopentelemetry-aspnetcore-instrumentation.md","幫 ASP.NET Core 接上 OpenTelemetry：從零到 Grafana／Tempo／Loki",{"type":7,"value":8,"toc":691},"minimark",[9,21,25,33,67,74,77,114,118,137,140,445,452,556,663,666,681,684,687],[10,11,12,13,20],"p",{},"分散式架構下服務一多（Message Queue、各種微服務、Restful／gRPC／GraphQL\nAPI），要追蹤 API 異常、改善流程、確認 SLI 指標，都需要一套可觀測性架構打\n底。這篇把「如何從零幫一個 ASP.NET Core 專案接上 OpenTelemetry、並串到\nGrafana 全家桶」整理成一份可以直接照抄的參考做法，完整可執行的範例放在\n",[14,15,19],"a",{"href":16,"rel":17},"https:\u002F\u002Fgithub.com\u002Ftom3019\u002FWeb-Application-Observability-and-Availability",[18],"nofollow","Web-Application-Observability-and-Availability","。",[22,23,24],"h2",{"id":24},"解決方案架構",[10,26,27,28,32],{},"以 ",[29,30,31],"strong",{},"OpenTelemetry（OTEL）"," 作為統一的可觀測性框架，搭配 Grafana 生態系：",[34,35,36,43,49,55,61],"ul",{},[37,38,39,42],"li",{},[29,40,41],{},"OpenTelemetry Collector","：收集 Trace 與 Metric 的集線器，可輸出到\nGrafana、Jaeger 等不同後端",[37,44,45,48],{},[29,46,47],{},"Prometheus","：儲存時序資料（CPU、Memory 等）的監控方案",[37,50,51,54],{},[29,52,53],{},"Grafana","：將 Prometheus、資料庫等來源的資料視覺化",[37,56,57,60],{},[29,58,59],{},"Grafana Tempo","：開源、可大規模擴展的分散式追蹤後端",[37,62,63,66],{},[29,64,65],{},"Grafana Loki","：可擴展、多租戶的日誌聚合系統",[10,68,69],{},[70,71],"img",{"alt":72,"src":73},"可觀測性解決方案架構","https:\u002F\u002Fraw.githubusercontent.com\u002Ftom3019\u002FWeb-Application-Observability-and-Availability\u002Fmaster\u002Fimages\u002F%E7%9B%A3%E6%8E%A7%E6%9E%B6%E6%A7%8B.png",[22,75,76],{"id":76},"本機執行",[78,79,84],"pre",{"className":80,"code":81,"language":82,"meta":83,"style":83},"language-shell shiki shiki-themes github-dark","# 先調整 configs\u002F 內各服務的 IP 與 Port\ndocker compose up -d\n","shell","",[85,86,87,96],"code",{"__ignoreMap":83},[88,89,92],"span",{"class":90,"line":91},"line",1,[88,93,95],{"class":94},"sAwPA","# 先調整 configs\u002F 內各服務的 IP 與 Port\n",[88,97,99,103,107,110],{"class":90,"line":98},2,[88,100,102],{"class":101},"svObZ","docker",[88,104,106],{"class":105},"sU2Wk"," compose",[88,108,109],{"class":105}," up",[88,111,113],{"class":112},"sDLfK"," -d\n",[22,115,117],{"id":116},"幫-aspnet-core-8-接上可觀測性","幫 ASP.NET Core 8 接上可觀測性",[10,119,120,121,124,125,124,128,124,131,124,134],{},"安裝套件：",[85,122,123],{},"OpenTelemetry.Extensions.Hosting","、\n",[85,126,127],{},"OpenTelemetry.Instrumentation.AspNetCore",[85,129,130],{},"OpenTelemetry.Instrumentation.Http",[85,132,133],{},"OpenTelemetry.Exporter.OpenTelemetryProtocol",[85,135,136],{},"OpenTelemetry.Exporter.Prometheus.AspNetCore",[10,138,139],{},"Tracing 與 Metrics 的註冊：",[78,141,145],{"className":142,"code":143,"language":144,"meta":83,"style":83},"language-csharp shiki shiki-themes github-dark","var otel = builder.Services.AddOpenTelemetry();\notel.ConfigureResource(resource => resource\n    .AddService(serviceName: builder.Environment.ApplicationName));\n\notel.WithMetrics(metrics => metrics\n    .AddAspNetCoreInstrumentation()\n    .AddMeter(greeterMeter.Name)\n    .AddMeter(\"Microsoft.AspNetCore.Hosting\")\n    .AddPrometheusExporter());\n\notel.WithTracing(tracing =>\n{\n    tracing.AddAspNetCoreInstrumentation(o =>\n    {\n        \u002F\u002F 從 X-Forwarded-For 或連線資訊解析真實客戶端 IP，寫入 span tag\n        o.EnrichWithHttpRequest = (activity, request) => { \u002F* ... *\u002F };\n    });\n    tracing.AddHttpClientInstrumentation();\n    tracing.AddSource(TracingActivitySource.WebApiRegisteredActivity.Name);\n    tracing.AddSource(TracingActivitySource.ServiceRegisteredActivity.Name);\n    tracing.AddOtlpExporter(o => o.Endpoint = new Uri(\"http:\u002F\u002Fotel:4317\"));\n});\n","csharp",[85,146,147,169,189,206,213,231,242,253,268,279,284,300,306,321,327,333,369,375,385,396,406,439],{"__ignoreMap":83},[88,148,149,153,156,159,163,166],{"class":90,"line":91},[88,150,152],{"class":151},"snl16","var",[88,154,155],{"class":101}," otel",[88,157,158],{"class":151}," =",[88,160,162],{"class":161},"s95oV"," builder.Services.",[88,164,165],{"class":101},"AddOpenTelemetry",[88,167,168],{"class":161},"();\n",[88,170,171,174,177,180,183,186],{"class":90,"line":98},[88,172,173],{"class":161},"otel.",[88,175,176],{"class":101},"ConfigureResource",[88,178,179],{"class":161},"(",[88,181,182],{"class":101},"resource",[88,184,185],{"class":151}," =>",[88,187,188],{"class":161}," resource\n",[88,190,192,195,198,200,203],{"class":90,"line":191},3,[88,193,194],{"class":161},"    .",[88,196,197],{"class":101},"AddService",[88,199,179],{"class":161},[88,201,202],{"class":101},"serviceName",[88,204,205],{"class":161},": builder.Environment.ApplicationName));\n",[88,207,209],{"class":90,"line":208},4,[88,210,212],{"emptyLinePlaceholder":211},true,"\n",[88,214,216,218,221,223,226,228],{"class":90,"line":215},5,[88,217,173],{"class":161},[88,219,220],{"class":101},"WithMetrics",[88,222,179],{"class":161},[88,224,225],{"class":101},"metrics",[88,227,185],{"class":151},[88,229,230],{"class":161}," metrics\n",[88,232,234,236,239],{"class":90,"line":233},6,[88,235,194],{"class":161},[88,237,238],{"class":101},"AddAspNetCoreInstrumentation",[88,240,241],{"class":161},"()\n",[88,243,245,247,250],{"class":90,"line":244},7,[88,246,194],{"class":161},[88,248,249],{"class":101},"AddMeter",[88,251,252],{"class":161},"(greeterMeter.Name)\n",[88,254,256,258,260,262,265],{"class":90,"line":255},8,[88,257,194],{"class":161},[88,259,249],{"class":101},[88,261,179],{"class":161},[88,263,264],{"class":105},"\"Microsoft.AspNetCore.Hosting\"",[88,266,267],{"class":161},")\n",[88,269,271,273,276],{"class":90,"line":270},9,[88,272,194],{"class":161},[88,274,275],{"class":101},"AddPrometheusExporter",[88,277,278],{"class":161},"());\n",[88,280,282],{"class":90,"line":281},10,[88,283,212],{"emptyLinePlaceholder":211},[88,285,287,289,292,294,297],{"class":90,"line":286},11,[88,288,173],{"class":161},[88,290,291],{"class":101},"WithTracing",[88,293,179],{"class":161},[88,295,296],{"class":101},"tracing",[88,298,299],{"class":151}," =>\n",[88,301,303],{"class":90,"line":302},12,[88,304,305],{"class":161},"{\n",[88,307,309,312,314,316,319],{"class":90,"line":308},13,[88,310,311],{"class":161},"    tracing.",[88,313,238],{"class":101},[88,315,179],{"class":161},[88,317,318],{"class":101},"o",[88,320,299],{"class":151},[88,322,324],{"class":90,"line":323},14,[88,325,326],{"class":161},"    {\n",[88,328,330],{"class":90,"line":329},15,[88,331,332],{"class":94},"        \u002F\u002F 從 X-Forwarded-For 或連線資訊解析真實客戶端 IP，寫入 span tag\n",[88,334,336,339,342,345,348,351,354,357,360,363,366],{"class":90,"line":335},16,[88,337,338],{"class":161},"        o.EnrichWithHttpRequest ",[88,340,341],{"class":151},"=",[88,343,344],{"class":161}," (",[88,346,347],{"class":101},"activity",[88,349,350],{"class":161},", ",[88,352,353],{"class":101},"request",[88,355,356],{"class":161},") ",[88,358,359],{"class":151},"=>",[88,361,362],{"class":161}," { ",[88,364,365],{"class":94},"\u002F* ... *\u002F",[88,367,368],{"class":161}," };\n",[88,370,372],{"class":90,"line":371},17,[88,373,374],{"class":161},"    });\n",[88,376,378,380,383],{"class":90,"line":377},18,[88,379,311],{"class":161},[88,381,382],{"class":101},"AddHttpClientInstrumentation",[88,384,168],{"class":161},[88,386,388,390,393],{"class":90,"line":387},19,[88,389,311],{"class":161},[88,391,392],{"class":101},"AddSource",[88,394,395],{"class":161},"(TracingActivitySource.WebApiRegisteredActivity.Name);\n",[88,397,399,401,403],{"class":90,"line":398},20,[88,400,311],{"class":161},[88,402,392],{"class":101},[88,404,405],{"class":161},"(TracingActivitySource.ServiceRegisteredActivity.Name);\n",[88,407,409,411,414,416,418,420,423,425,428,431,433,436],{"class":90,"line":408},21,[88,410,311],{"class":161},[88,412,413],{"class":101},"AddOtlpExporter",[88,415,179],{"class":161},[88,417,318],{"class":101},[88,419,185],{"class":151},[88,421,422],{"class":161}," o.Endpoint ",[88,424,341],{"class":151},[88,426,427],{"class":151}," new",[88,429,430],{"class":101}," Uri",[88,432,179],{"class":161},[88,434,435],{"class":105},"\"http:\u002F\u002Fotel:4317\"",[88,437,438],{"class":161},"));\n",[88,440,442],{"class":90,"line":441},22,[88,443,444],{"class":161},"});\n",[10,446,447,448,451],{},"分層命名 ",[85,449,450],{},"ActivitySource","，讓一次請求跨 WebApi／Service／Repository 三層的\nTrace 都能被串起來：",[78,453,455],{"className":142,"code":454,"language":144,"meta":83,"style":83},"public static class TracingActivitySource\n{\n    public static readonly ActivitySource WebApiRegisteredActivity = new(\"Monitor.Sample.Api\");\n    public static readonly ActivitySource ServiceRegisteredActivity = new(\"Monitor.Sample.Service\");\n    public static readonly ActivitySource RepositoryRegisteredActivity = new(\"Monitor.Sample.Repository\");\n}\n",[85,456,457,471,475,503,527,551],{"__ignoreMap":83},[88,458,459,462,465,468],{"class":90,"line":91},[88,460,461],{"class":151},"public",[88,463,464],{"class":151}," static",[88,466,467],{"class":151}," class",[88,469,470],{"class":101}," TracingActivitySource\n",[88,472,473],{"class":90,"line":98},[88,474,305],{"class":161},[88,476,477,480,482,485,488,491,493,495,497,500],{"class":90,"line":191},[88,478,479],{"class":151},"    public",[88,481,464],{"class":151},[88,483,484],{"class":151}," readonly",[88,486,487],{"class":101}," ActivitySource",[88,489,490],{"class":101}," WebApiRegisteredActivity",[88,492,158],{"class":151},[88,494,427],{"class":151},[88,496,179],{"class":161},[88,498,499],{"class":105},"\"Monitor.Sample.Api\"",[88,501,502],{"class":161},");\n",[88,504,505,507,509,511,513,516,518,520,522,525],{"class":90,"line":208},[88,506,479],{"class":151},[88,508,464],{"class":151},[88,510,484],{"class":151},[88,512,487],{"class":101},[88,514,515],{"class":101}," ServiceRegisteredActivity",[88,517,158],{"class":151},[88,519,427],{"class":151},[88,521,179],{"class":161},[88,523,524],{"class":105},"\"Monitor.Sample.Service\"",[88,526,502],{"class":161},[88,528,529,531,533,535,537,540,542,544,546,549],{"class":90,"line":215},[88,530,479],{"class":151},[88,532,464],{"class":151},[88,534,484],{"class":151},[88,536,487],{"class":101},[88,538,539],{"class":101}," RepositoryRegisteredActivity",[88,541,158],{"class":151},[88,543,427],{"class":151},[88,545,179],{"class":161},[88,547,548],{"class":105},"\"Monitor.Sample.Repository\"",[88,550,502],{"class":161},[88,552,553],{"class":90,"line":233},[88,554,555],{"class":161},"}\n",[78,557,559],{"className":142,"code":558,"language":144,"meta":83,"style":83},"public Task\u003Cint> GetNumberAsync()\n{\n    using (TracingActivitySource.ServiceRegisteredActivity.StartActivity(\n        $\"{nameof(TracingTestService)}.{nameof(GetNumberAsync)}\"))\n    {\n        return tracingTestRepository.GetNumberAsync();\n    }\n}\n",[85,560,561,582,586,600,638,642,654,659],{"__ignoreMap":83},[88,562,563,565,568,571,574,577,580],{"class":90,"line":91},[88,564,461],{"class":151},[88,566,567],{"class":101}," Task",[88,569,570],{"class":161},"\u003C",[88,572,573],{"class":151},"int",[88,575,576],{"class":161},"> ",[88,578,579],{"class":101},"GetNumberAsync",[88,581,241],{"class":161},[88,583,584],{"class":90,"line":98},[88,585,305],{"class":161},[88,587,588,591,594,597],{"class":90,"line":191},[88,589,590],{"class":151},"    using",[88,592,593],{"class":161}," (TracingActivitySource.ServiceRegisteredActivity.",[88,595,596],{"class":101},"StartActivity",[88,598,599],{"class":161},"(\n",[88,601,602,605,608,611,613,616,619,622,624,626,628,630,632,635],{"class":90,"line":208},[88,603,604],{"class":105},"        $\"",[88,606,607],{"class":105},"{",[88,609,610],{"class":151},"nameof",[88,612,179],{"class":105},[88,614,615],{"class":161},"TracingTestService",[88,617,618],{"class":105},")}",[88,620,621],{"class":105},".",[88,623,607],{"class":105},[88,625,610],{"class":151},[88,627,179],{"class":105},[88,629,579],{"class":161},[88,631,618],{"class":105},[88,633,634],{"class":105},"\"",[88,636,637],{"class":161},"))\n",[88,639,640],{"class":90,"line":215},[88,641,326],{"class":161},[88,643,644,647,650,652],{"class":90,"line":233},[88,645,646],{"class":151},"        return",[88,648,649],{"class":161}," tracingTestRepository.",[88,651,579],{"class":101},[88,653,168],{"class":161},[88,655,656],{"class":90,"line":244},[88,657,658],{"class":161},"    }\n",[88,660,661],{"class":90,"line":255},[88,662,555],{"class":161},[22,664,665],{"id":665},"踩過的坑",[667,668,669],"blockquote",{},[10,670,671,672,674,675,680],{},"⚠️ ",[85,673,136],{}," 版本 1.8.0 會導致\nPrometheus 收集 log 錯誤，需改回 1.7.0 或更新版本才能解決\n（",[14,676,679],{"href":677,"rel":678},"https:\u002F\u002Fgithub.com\u002Fopen-telemetry\u002Fopentelemetry-dotnet\u002Fissues\u002F5506",[18],"對應 issue","）。",[22,682,683],{"id":683},"延伸應用",[10,685,686],{},"這次在範例專案上把 Traces、Logs、Metrics 三條線都串起來了，下一步是套用到正式\n環境：統一告警儀表板，並依業務情境挑選合適的 Prometheus 指標類型（Counter、\nGauge、Histogram），讓告警規則與 PromQL 查詢有一致的設計基礎。",[688,689,690],"style",{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}",{"title":83,"searchDepth":98,"depth":98,"links":692},[693,694,695,696,697],{"id":24,"depth":98,"text":24},{"id":76,"depth":98,"text":76},{"id":116,"depth":98,"text":117},{"id":665,"depth":98,"text":665},{"id":683,"depth":98,"text":683},"2024-08-19","分散式架構下服務一多（Message Queue、各種微服務、Restful／gRPC／GraphQL\nAPI），要追蹤 API 異常、改善流程、確認 SLI 指標，都需要一套可觀測性架構打\n底。這篇把「如何從零幫一個 ASP.NET Core 專案接上 OpenTelemetry、並串到\nGrafana 全家桶」整理成一份可以直接照抄的參考做法，完整可執行的範例放在\nWeb-Application-Observability-and-Availability。",null,"md",{},"\u002Farticles\u002Fopentelemetry-aspnetcore-instrumentation",{"title":5,"description":699},"articles\u002Fopentelemetry-aspnetcore-instrumentation",[707,53,708,709],"OpenTelemetry","Observability","ASP.NET Core","GPulDgpwrZCPTPRVgZlb_RD1UvR2aFEt83JeSIeYB4o",1792959150353]