[{"data":1,"prerenderedAt":1131},["ShallowReactive",2],{"blog-\u002Fblog\u002Fdotnet-logging-monitoring":3},{"id":4,"title":5,"body":6,"description":1116,"difficulty":1117,"extension":1118,"framework":1119,"frameworkSlug":1120,"meta":1121,"navigation":90,"order":69,"path":1122,"qaPath":1123,"seo":1124,"stem":1125,"subtopic":1126,"topic":1127,"topicSlug":1128,"updated":1129,"__hash__":1130},"blog\u002Fblog\u002Fdotnet-logging-monitoring.md","Structured Logging and Monitoring in ASP.NET Core",{"type":7,"value":8,"toc":1104},"minimark",[9,14,32,36,50,197,212,216,219,287,346,358,362,368,436,439,443,449,565,571,575,582,657,668,672,679,753,756,760,763,853,856,860,863,1014,1018,1025,1080,1087,1100],[10,11,13],"h2",{"id":12},"why-logging-and-monitoring-matter-in-net-interviews","Why logging and monitoring matter in .NET interviews",[15,16,17,18,22,23,27,28,31],"p",{},"Production systems that cannot be observed cannot be debugged. Interviewers probe\nlogging to check whether a candidate understands ",[19,20,21],"em",{},"structured"," logging (not just\n",[24,25,26],"code",{},"Console.WriteLine","), knows how to correlate logs across a request with scopes,\nand can configure health checks that make sense for a Kubernetes deployment.\nThis article walks through the full stack — from ",[24,29,30],{},"ILogger\u003CT>"," basics to\nOpenTelemetry distributed tracing.",[10,33,35],{"id":34},"ilogger-the-built-in-abstraction","ILogger: the built-in abstraction",[15,37,38,40,41,44,45,49],{},[24,39,30],{}," is the standard .NET logging interface. The generic parameter ",[24,42,43],{},"T","\nbecomes the ",[46,47,48],"strong",{},"category name"," — the source identifier that appears in every log\nentry and lets you filter by class or namespace.",[51,52,57],"pre",{"className":53,"code":54,"language":55,"meta":56,"style":56},"language-csharp shiki shiki-themes github-light github-dark","public class OrderService\n{\n    private readonly ILogger\u003COrderService> _logger;\n    public OrderService(ILogger\u003COrderService> logger) => _logger = logger;\n\n    public async Task\u003COrder> PlaceOrderAsync(Order order)\n    {\n        _logger.LogInformation(\"Placing order {OrderId} for {CustomerId}\",\n            order.Id, order.CustomerId);\n\n        try\n        {\n            var result = await _repo.SaveAsync(order);\n            _logger.LogInformation(\"Order {OrderId} saved\", result.Id);\n            return result;\n        }\n        catch (Exception ex)\n        {\n            _logger.LogError(ex, \"Failed to place order {OrderId}\", order.Id);\n            throw;\n        }\n    }\n}\n","csharp","",[24,58,59,67,73,79,85,92,98,104,110,116,121,127,133,139,145,151,157,163,168,174,180,185,191],{"__ignoreMap":56},[60,61,64],"span",{"class":62,"line":63},"line",1,[60,65,66],{},"public class OrderService\n",[60,68,70],{"class":62,"line":69},2,[60,71,72],{},"{\n",[60,74,76],{"class":62,"line":75},3,[60,77,78],{},"    private readonly ILogger\u003COrderService> _logger;\n",[60,80,82],{"class":62,"line":81},4,[60,83,84],{},"    public OrderService(ILogger\u003COrderService> logger) => _logger = logger;\n",[60,86,88],{"class":62,"line":87},5,[60,89,91],{"emptyLinePlaceholder":90},true,"\n",[60,93,95],{"class":62,"line":94},6,[60,96,97],{},"    public async Task\u003COrder> PlaceOrderAsync(Order order)\n",[60,99,101],{"class":62,"line":100},7,[60,102,103],{},"    {\n",[60,105,107],{"class":62,"line":106},8,[60,108,109],{},"        _logger.LogInformation(\"Placing order {OrderId} for {CustomerId}\",\n",[60,111,113],{"class":62,"line":112},9,[60,114,115],{},"            order.Id, order.CustomerId);\n",[60,117,119],{"class":62,"line":118},10,[60,120,91],{"emptyLinePlaceholder":90},[60,122,124],{"class":62,"line":123},11,[60,125,126],{},"        try\n",[60,128,130],{"class":62,"line":129},12,[60,131,132],{},"        {\n",[60,134,136],{"class":62,"line":135},13,[60,137,138],{},"            var result = await _repo.SaveAsync(order);\n",[60,140,142],{"class":62,"line":141},14,[60,143,144],{},"            _logger.LogInformation(\"Order {OrderId} saved\", result.Id);\n",[60,146,148],{"class":62,"line":147},15,[60,149,150],{},"            return result;\n",[60,152,154],{"class":62,"line":153},16,[60,155,156],{},"        }\n",[60,158,160],{"class":62,"line":159},17,[60,161,162],{},"        catch (Exception ex)\n",[60,164,166],{"class":62,"line":165},18,[60,167,132],{},[60,169,171],{"class":62,"line":170},19,[60,172,173],{},"            _logger.LogError(ex, \"Failed to place order {OrderId}\", order.Id);\n",[60,175,177],{"class":62,"line":176},20,[60,178,179],{},"            throw;\n",[60,181,183],{"class":62,"line":182},21,[60,184,156],{},[60,186,188],{"class":62,"line":187},22,[60,189,190],{},"    }\n",[60,192,194],{"class":62,"line":193},23,[60,195,196],{},"}\n",[15,198,199,200,203,204,207,208,211],{},"Use ",[46,201,202],{},"message templates"," (",[24,205,206],{},"{OrderId}","), never string interpolation (",[24,209,210],{},"$\"{order.Id}\"",").\nTemplates let structured logging providers store each placeholder as a separate\nqueryable field — the difference between a plain string and a searchable document.",[10,213,215],{"id":214},"log-levels-and-when-to-use-them","Log levels and when to use them",[15,217,218],{},".NET defines six levels in ascending severity. The configured minimum level\nfilters out everything below it, so disabled levels cost almost nothing at runtime.",[220,221,222,235],"table",{},[223,224,225],"thead",{},[226,227,228,232],"tr",{},[229,230,231],"th",{},"Level",[229,233,234],{},"When to log",[236,237,238,247,255,263,271,279],"tbody",{},[226,239,240,244],{},[241,242,243],"td",{},"Trace",[241,245,246],{},"Step-by-step traces for deep debugging only",[226,248,249,252],{},[241,250,251],{},"Debug",[241,253,254],{},"Developer diagnostics (cache state, loop variables)",[226,256,257,260],{},[241,258,259],{},"Information",[241,261,262],{},"Normal business events (order placed, user logged in)",[226,264,265,268],{},[241,266,267],{},"Warning",[241,269,270],{},"Handled unexpected conditions (retry, degraded mode)",[226,272,273,276],{},[241,274,275],{},"Error",[241,277,278],{},"Unhandled failures requiring investigation",[226,280,281,284],{},[241,282,283],{},"Critical",[241,285,286],{},"System-wide failures requiring immediate action",[51,288,290],{"className":53,"code":289,"language":55,"meta":56,"style":56},"\u002F\u002F Configuration in appsettings.json:\n\u002F\u002F {\n\u002F\u002F \"Logging\": {\n\u002F\u002F \"LogLevel\": {\n\u002F\u002F \"Default\":             \"Information\",\n\u002F\u002F \"Microsoft.AspNetCore\": \"Warning\",   \u002F\u002F suppress noisy framework logs\n\u002F\u002F \"MyApp.Data\":          \"Debug\"       \u002F\u002F verbose for one namespace\n\u002F\u002F }\n\u002F\u002F }\n\u002F\u002F }\n",[24,291,292,297,302,307,312,317,325,333,338,342],{"__ignoreMap":56},[60,293,294],{"class":62,"line":63},[60,295,296],{},"\u002F\u002F Configuration in appsettings.json:\n",[60,298,299],{"class":62,"line":69},[60,300,301],{},"\u002F\u002F {\n",[60,303,304],{"class":62,"line":75},[60,305,306],{},"\u002F\u002F \"Logging\": {\n",[60,308,309],{"class":62,"line":81},[60,310,311],{},"\u002F\u002F \"LogLevel\": {\n",[60,313,314],{"class":62,"line":87},[60,315,316],{},"\u002F\u002F \"Default\":             \"Information\",\n",[60,318,319,322],{"class":62,"line":94},[60,320,321],{},"\u002F\u002F \"Microsoft.AspNetCore\": \"Warning\",",[60,323,324],{},"   \u002F\u002F suppress noisy framework logs\n",[60,326,327,330],{"class":62,"line":100},[60,328,329],{},"\u002F\u002F \"MyApp.Data\":          \"Debug\"",[60,331,332],{},"       \u002F\u002F verbose for one namespace\n",[60,334,335],{"class":62,"line":106},[60,336,337],{},"\u002F\u002F }\n",[60,339,340],{"class":62,"line":112},[60,341,337],{},[60,343,344],{"class":62,"line":118},[60,345,337],{},[15,347,348,349,351,352,354,355,357],{},"Log ",[24,350,259],{}," for every significant business event — these form the audit trail.\nLog ",[24,353,267],{}," when you handle an error gracefully. Log ",[24,356,275],{}," for failures that\na human needs to investigate.",[10,359,361],{"id":360},"structured-logging-why-it-matters","Structured logging: why it matters",[15,363,364,367],{},[46,365,366],{},"Structured logging"," stores log entries as key-value documents. This transforms\nlogs from a write-only archive into a queryable dataset.",[51,369,371],{"className":53,"code":370,"language":55,"meta":56,"style":56},"\u002F\u002F Plain text — unsearchable:\n_logger.LogInformation($\"Order {order.Id} placed for customer {order.CustomerId}\");\n\u002F\u002F Stored as: \"Order 42 placed for customer 99\"\n\n\u002F\u002F Structured — each placeholder is a separate field:\n_logger.LogInformation(\"Order {OrderId} placed for {CustomerId}, total {Total:C}\",\n    order.Id, order.CustomerId, order.Total);\n\u002F\u002F Stored as: { \"OrderId\": 42, \"CustomerId\": 99, \"Total\": 150.00, \"@l\": \"Information\", ... }\n\n\u002F\u002F Now queryable in Seq or Kibana:\n\u002F\u002F OrderId = 42\n\u002F\u002F Total > 100 AND @l = \"Error\"\n\u002F\u002F CustomerId = 99 AND @t > \"2026-06-01T00:00:00\"\n",[24,372,373,378,383,388,392,397,402,407,412,416,421,426,431],{"__ignoreMap":56},[60,374,375],{"class":62,"line":63},[60,376,377],{},"\u002F\u002F Plain text — unsearchable:\n",[60,379,380],{"class":62,"line":69},[60,381,382],{},"_logger.LogInformation($\"Order {order.Id} placed for customer {order.CustomerId}\");\n",[60,384,385],{"class":62,"line":75},[60,386,387],{},"\u002F\u002F Stored as: \"Order 42 placed for customer 99\"\n",[60,389,390],{"class":62,"line":81},[60,391,91],{"emptyLinePlaceholder":90},[60,393,394],{"class":62,"line":87},[60,395,396],{},"\u002F\u002F Structured — each placeholder is a separate field:\n",[60,398,399],{"class":62,"line":94},[60,400,401],{},"_logger.LogInformation(\"Order {OrderId} placed for {CustomerId}, total {Total:C}\",\n",[60,403,404],{"class":62,"line":100},[60,405,406],{},"    order.Id, order.CustomerId, order.Total);\n",[60,408,409],{"class":62,"line":106},[60,410,411],{},"\u002F\u002F Stored as: { \"OrderId\": 42, \"CustomerId\": 99, \"Total\": 150.00, \"@l\": \"Information\", ... }\n",[60,413,414],{"class":62,"line":112},[60,415,91],{"emptyLinePlaceholder":90},[60,417,418],{"class":62,"line":118},[60,419,420],{},"\u002F\u002F Now queryable in Seq or Kibana:\n",[60,422,423],{"class":62,"line":123},[60,424,425],{},"\u002F\u002F OrderId = 42\n",[60,427,428],{"class":62,"line":129},[60,429,430],{},"\u002F\u002F Total > 100 AND @l = \"Error\"\n",[60,432,433],{"class":62,"line":135},[60,434,435],{},"\u002F\u002F CustomerId = 99 AND @t > \"2026-06-01T00:00:00\"\n",[15,437,438],{},"Structured logging answers \"how many orders over $100 failed last Thursday?\" without\nparsing strings — something impossible with plain text logs.",[10,440,442],{"id":441},"serilog-the-most-popular-net-logging-library","Serilog: the most popular .NET logging library",[15,444,445,446,448],{},"Serilog adds rich sink support (files, Seq, Elasticsearch, Application Insights)\nwhile integrating transparently with ",[24,447,30],{}," so no application code changes.",[51,450,452],{"className":53,"code":451,"language":55,"meta":56,"style":56},"\u002F\u002F dotnet add package Serilog.AspNetCore Serilog.Sinks.Console Serilog.Sinks.File\n\n\u002F\u002F Program.cs:\nLog.Logger = new LoggerConfiguration()\n    .MinimumLevel.Information()\n    .MinimumLevel.Override(\"Microsoft.AspNetCore\", LogEventLevel.Warning)\n    .Enrich.FromLogContext()       \u002F\u002F picks up log scope properties\n    .Enrich.WithMachineName()\n    .WriteTo.Console(new JsonFormatter())\n    .WriteTo.File(\"logs\u002Fapp-.log\", rollingInterval: RollingInterval.Day)\n    .WriteTo.Seq(\"http:\u002F\u002Fseq:5341\")\n    .CreateLogger();\n\nbuilder.Host.UseSerilog();\n\n\u002F\u002F One structured log line per HTTP request (replaces two noisy default events):\napp.UseSerilogRequestLogging(opts =>\n{\n    opts.MessageTemplate =\n        \"{RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms\";\n    opts.EnrichDiagnosticContext = (diagCtx, httpCtx) =>\n        diagCtx.Set(\"UserId\", httpCtx.User.FindFirst(\"sub\")?.Value ?? \"anon\");\n});\n",[24,453,454,459,463,468,473,478,483,488,493,498,503,508,513,517,522,526,531,536,540,545,550,555,560],{"__ignoreMap":56},[60,455,456],{"class":62,"line":63},[60,457,458],{},"\u002F\u002F dotnet add package Serilog.AspNetCore Serilog.Sinks.Console Serilog.Sinks.File\n",[60,460,461],{"class":62,"line":69},[60,462,91],{"emptyLinePlaceholder":90},[60,464,465],{"class":62,"line":75},[60,466,467],{},"\u002F\u002F Program.cs:\n",[60,469,470],{"class":62,"line":81},[60,471,472],{},"Log.Logger = new LoggerConfiguration()\n",[60,474,475],{"class":62,"line":87},[60,476,477],{},"    .MinimumLevel.Information()\n",[60,479,480],{"class":62,"line":94},[60,481,482],{},"    .MinimumLevel.Override(\"Microsoft.AspNetCore\", LogEventLevel.Warning)\n",[60,484,485],{"class":62,"line":100},[60,486,487],{},"    .Enrich.FromLogContext()       \u002F\u002F picks up log scope properties\n",[60,489,490],{"class":62,"line":106},[60,491,492],{},"    .Enrich.WithMachineName()\n",[60,494,495],{"class":62,"line":112},[60,496,497],{},"    .WriteTo.Console(new JsonFormatter())\n",[60,499,500],{"class":62,"line":118},[60,501,502],{},"    .WriteTo.File(\"logs\u002Fapp-.log\", rollingInterval: RollingInterval.Day)\n",[60,504,505],{"class":62,"line":123},[60,506,507],{},"    .WriteTo.Seq(\"http:\u002F\u002Fseq:5341\")\n",[60,509,510],{"class":62,"line":129},[60,511,512],{},"    .CreateLogger();\n",[60,514,515],{"class":62,"line":135},[60,516,91],{"emptyLinePlaceholder":90},[60,518,519],{"class":62,"line":141},[60,520,521],{},"builder.Host.UseSerilog();\n",[60,523,524],{"class":62,"line":147},[60,525,91],{"emptyLinePlaceholder":90},[60,527,528],{"class":62,"line":153},[60,529,530],{},"\u002F\u002F One structured log line per HTTP request (replaces two noisy default events):\n",[60,532,533],{"class":62,"line":159},[60,534,535],{},"app.UseSerilogRequestLogging(opts =>\n",[60,537,538],{"class":62,"line":165},[60,539,72],{},[60,541,542],{"class":62,"line":170},[60,543,544],{},"    opts.MessageTemplate =\n",[60,546,547],{"class":62,"line":176},[60,548,549],{},"        \"{RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms\";\n",[60,551,552],{"class":62,"line":182},[60,553,554],{},"    opts.EnrichDiagnosticContext = (diagCtx, httpCtx) =>\n",[60,556,557],{"class":62,"line":187},[60,558,559],{},"        diagCtx.Set(\"UserId\", httpCtx.User.FindFirst(\"sub\")?.Value ?? \"anon\");\n",[60,561,562],{"class":62,"line":193},[60,563,564],{},"});\n",[15,566,567,570],{},[24,568,569],{},"UseSerilogRequestLogging()"," replaces ASP.NET Core's default request logging\n(which emits two events per request) with one structured event that includes\ntiming and the authenticated user ID.",[10,572,574],{"id":573},"log-scopes-ambient-context-without-parameter-passing","Log scopes: ambient context without parameter passing",[15,576,577,578,581],{},"Log scopes add properties to every log entry within a ",[24,579,580],{},"using"," block. This is\nthe cleanest way to attach a request ID or correlation ID without threading it\nthrough every method signature.",[51,583,585],{"className":53,"code":584,"language":55,"meta":56,"style":56},"public async Task\u003COrder> PlaceOrderAsync(Order order)\n{\n    using (_logger.BeginScope(new Dictionary\u003Cstring, object>\n    {\n        [\"OrderId\"]       = order.Id,\n        [\"CorrelationId\"] = Activity.Current?.Id ?? Guid.NewGuid().ToString(),\n    }))\n    {\n        \u002F\u002F Every log in this block carries OrderId + CorrelationId:\n        _logger.LogInformation(\"Validating\");\n        await ValidateAsync(order);\n        _logger.LogInformation(\"Saving\");\n        await _repo.SaveAsync(order);\n    }\n}\n",[24,586,587,592,596,601,605,610,615,620,624,629,634,639,644,649,653],{"__ignoreMap":56},[60,588,589],{"class":62,"line":63},[60,590,591],{},"public async Task\u003COrder> PlaceOrderAsync(Order order)\n",[60,593,594],{"class":62,"line":69},[60,595,72],{},[60,597,598],{"class":62,"line":75},[60,599,600],{},"    using (_logger.BeginScope(new Dictionary\u003Cstring, object>\n",[60,602,603],{"class":62,"line":81},[60,604,103],{},[60,606,607],{"class":62,"line":87},[60,608,609],{},"        [\"OrderId\"]       = order.Id,\n",[60,611,612],{"class":62,"line":94},[60,613,614],{},"        [\"CorrelationId\"] = Activity.Current?.Id ?? Guid.NewGuid().ToString(),\n",[60,616,617],{"class":62,"line":100},[60,618,619],{},"    }))\n",[60,621,622],{"class":62,"line":106},[60,623,103],{},[60,625,626],{"class":62,"line":112},[60,627,628],{},"        \u002F\u002F Every log in this block carries OrderId + CorrelationId:\n",[60,630,631],{"class":62,"line":118},[60,632,633],{},"        _logger.LogInformation(\"Validating\");\n",[60,635,636],{"class":62,"line":123},[60,637,638],{},"        await ValidateAsync(order);\n",[60,640,641],{"class":62,"line":129},[60,642,643],{},"        _logger.LogInformation(\"Saving\");\n",[60,645,646],{"class":62,"line":135},[60,647,648],{},"        await _repo.SaveAsync(order);\n",[60,650,651],{"class":62,"line":141},[60,652,190],{},[60,654,655],{"class":62,"line":147},[60,656,196],{},[15,658,659,660,663,664,667],{},"In Serilog, ",[24,661,662],{},".Enrich.FromLogContext()"," is required to pick up scope properties.\nIn the built-in provider, ",[24,665,666],{},"IncludeScopes: true"," must be set in the logging config.",[10,669,671],{"id":670},"health-checks-liveness-and-readiness","Health checks: liveness and readiness",[15,673,674,675,678],{},"ASP.NET Core's health checks expose ",[24,676,677],{},"\u002Fhealth"," endpoints that orchestrators probe to\ndecide whether to route traffic to an instance. The two probe types serve different\npurposes.",[51,680,682],{"className":53,"code":681,"language":55,"meta":56,"style":56},"builder.Services.AddHealthChecks()\n    .AddDbContextCheck\u003CAppDbContext>(tags: [\"readiness\"])\n    .AddRedis(\"localhost:6379\",      tags: [\"readiness\"]);\n\n\u002F\u002F Liveness — is the process running? No dependency checks:\napp.MapHealthChecks(\"\u002Fhealth\u002Flive\", new HealthCheckOptions\n{\n    Predicate = _ => false, \u002F\u002F always 200 if the process is alive\n});\n\n\u002F\u002F Readiness — can this instance handle traffic?\napp.MapHealthChecks(\"\u002Fhealth\u002Fready\", new HealthCheckOptions\n{\n    Predicate = c => c.Tags.Contains(\"readiness\"),\n});\n",[24,683,684,689,694,699,703,708,713,717,722,726,730,735,740,744,749],{"__ignoreMap":56},[60,685,686],{"class":62,"line":63},[60,687,688],{},"builder.Services.AddHealthChecks()\n",[60,690,691],{"class":62,"line":69},[60,692,693],{},"    .AddDbContextCheck\u003CAppDbContext>(tags: [\"readiness\"])\n",[60,695,696],{"class":62,"line":75},[60,697,698],{},"    .AddRedis(\"localhost:6379\",      tags: [\"readiness\"]);\n",[60,700,701],{"class":62,"line":81},[60,702,91],{"emptyLinePlaceholder":90},[60,704,705],{"class":62,"line":87},[60,706,707],{},"\u002F\u002F Liveness — is the process running? No dependency checks:\n",[60,709,710],{"class":62,"line":94},[60,711,712],{},"app.MapHealthChecks(\"\u002Fhealth\u002Flive\", new HealthCheckOptions\n",[60,714,715],{"class":62,"line":100},[60,716,72],{},[60,718,719],{"class":62,"line":106},[60,720,721],{},"    Predicate = _ => false, \u002F\u002F always 200 if the process is alive\n",[60,723,724],{"class":62,"line":112},[60,725,564],{},[60,727,728],{"class":62,"line":118},[60,729,91],{"emptyLinePlaceholder":90},[60,731,732],{"class":62,"line":123},[60,733,734],{},"\u002F\u002F Readiness — can this instance handle traffic?\n",[60,736,737],{"class":62,"line":129},[60,738,739],{},"app.MapHealthChecks(\"\u002Fhealth\u002Fready\", new HealthCheckOptions\n",[60,741,742],{"class":62,"line":135},[60,743,72],{},[60,745,746],{"class":62,"line":141},[60,747,748],{},"    Predicate = c => c.Tags.Contains(\"readiness\"),\n",[60,750,751],{"class":62,"line":147},[60,752,564],{},[15,754,755],{},"Liveness probe failure causes Kubernetes to restart the pod. Readiness probe failure\nremoves it from the load balancer without restarting it. A database being down\nshould make the pod unready (not unalive) — the pod can recover without a restart\nonce the database comes back.",[10,757,759],{"id":758},"custom-metrics-with-systemdiagnosticsmetrics","Custom metrics with System.Diagnostics.Metrics",[15,761,762],{},".NET 8+ has a built-in metrics API that exports to Prometheus, OpenTelemetry, or\nApplication Insights without changing application code.",[51,764,766],{"className":53,"code":765,"language":55,"meta":56,"style":56},"\u002F\u002F Define metrics once as static fields:\nprivate static readonly Meter   _meter = new(\"MyApp.Orders\", \"1.0.0\");\nprivate static readonly Counter\u003Clong>    _placed  = _meter.CreateCounter\u003Clong>(\"orders.placed\");\nprivate static readonly Histogram\u003Cdouble> _total  = _meter.CreateHistogram\u003Cdouble>(\"orders.total.usd\");\n\n\u002F\u002F Emit in application code:\npublic async Task\u003COrder> PlaceOrderAsync(Order order)\n{\n    var result = await _repo.SaveAsync(order);\n    _placed.Add(1, new TagList { { \"channel\", order.Channel } });\n    _total.Record((double)order.Total, new TagList { { \"region\", order.Region } });\n    return result;\n}\n\n\u002F\u002F Export to Prometheus:\nbuilder.Services.AddOpenTelemetry()\n    .WithMetrics(m => m.AddMeter(\"MyApp.Orders\").AddPrometheusExporter());\napp.MapPrometheusScrapingEndpoint(\"\u002Fmetrics\");\n",[24,767,768,773,778,783,788,792,797,801,805,810,815,820,825,829,833,838,843,848],{"__ignoreMap":56},[60,769,770],{"class":62,"line":63},[60,771,772],{},"\u002F\u002F Define metrics once as static fields:\n",[60,774,775],{"class":62,"line":69},[60,776,777],{},"private static readonly Meter   _meter = new(\"MyApp.Orders\", \"1.0.0\");\n",[60,779,780],{"class":62,"line":75},[60,781,782],{},"private static readonly Counter\u003Clong>    _placed  = _meter.CreateCounter\u003Clong>(\"orders.placed\");\n",[60,784,785],{"class":62,"line":81},[60,786,787],{},"private static readonly Histogram\u003Cdouble> _total  = _meter.CreateHistogram\u003Cdouble>(\"orders.total.usd\");\n",[60,789,790],{"class":62,"line":87},[60,791,91],{"emptyLinePlaceholder":90},[60,793,794],{"class":62,"line":94},[60,795,796],{},"\u002F\u002F Emit in application code:\n",[60,798,799],{"class":62,"line":100},[60,800,591],{},[60,802,803],{"class":62,"line":106},[60,804,72],{},[60,806,807],{"class":62,"line":112},[60,808,809],{},"    var result = await _repo.SaveAsync(order);\n",[60,811,812],{"class":62,"line":118},[60,813,814],{},"    _placed.Add(1, new TagList { { \"channel\", order.Channel } });\n",[60,816,817],{"class":62,"line":123},[60,818,819],{},"    _total.Record((double)order.Total, new TagList { { \"region\", order.Region } });\n",[60,821,822],{"class":62,"line":129},[60,823,824],{},"    return result;\n",[60,826,827],{"class":62,"line":135},[60,828,196],{},[60,830,831],{"class":62,"line":141},[60,832,91],{"emptyLinePlaceholder":90},[60,834,835],{"class":62,"line":147},[60,836,837],{},"\u002F\u002F Export to Prometheus:\n",[60,839,840],{"class":62,"line":153},[60,841,842],{},"builder.Services.AddOpenTelemetry()\n",[60,844,845],{"class":62,"line":159},[60,846,847],{},"    .WithMetrics(m => m.AddMeter(\"MyApp.Orders\").AddPrometheusExporter());\n",[60,849,850],{"class":62,"line":165},[60,851,852],{},"app.MapPrometheusScrapingEndpoint(\"\u002Fmetrics\");\n",[15,854,855],{},"Define meters and instruments as static fields — creating them per-request causes\nduplicate registration errors and is expensive.",[10,857,859],{"id":858},"distributed-tracing-with-opentelemetry","Distributed tracing with OpenTelemetry",[15,861,862],{},"Distributed tracing tracks a request as it flows through multiple services,\ncreating a timeline that shows exactly where time was spent and where errors\noccurred.",[51,864,866],{"className":53,"code":865,"language":55,"meta":56,"style":56},"builder.Services.AddOpenTelemetry()\n    .WithTracing(t => t\n        .AddSource(\"MyApp.Orders\")\n        .AddAspNetCoreInstrumentation()\n        .AddHttpClientInstrumentation()\n        .AddEntityFrameworkCoreInstrumentation()\n        .AddJaegerExporter(j => j.AgentHost = \"jaeger\"));\n\n\u002F\u002F Custom span for a business operation:\nprivate static readonly ActivitySource _src = new(\"MyApp.Orders\");\n\npublic async Task\u003COrder> PlaceOrderAsync(Order order)\n{\n    using var span = _src.StartActivity(\"PlaceOrder\");\n    span?.SetTag(\"order.id\", order.Id);\n    span?.SetTag(\"customer.id\", order.CustomerId);\n\n    try\n    {\n        var result = await _repo.SaveAsync(order);\n        span?.SetStatus(ActivityStatusCode.Ok);\n        return result;\n    }\n    catch (Exception ex)\n    {\n        span?.SetStatus(ActivityStatusCode.Error, ex.Message);\n        span?.RecordException(ex);\n        throw;\n    }\n}\n",[24,867,868,872,877,882,887,892,897,902,906,911,916,920,924,928,933,938,943,947,952,956,961,966,971,975,981,986,992,998,1004,1009],{"__ignoreMap":56},[60,869,870],{"class":62,"line":63},[60,871,842],{},[60,873,874],{"class":62,"line":69},[60,875,876],{},"    .WithTracing(t => t\n",[60,878,879],{"class":62,"line":75},[60,880,881],{},"        .AddSource(\"MyApp.Orders\")\n",[60,883,884],{"class":62,"line":81},[60,885,886],{},"        .AddAspNetCoreInstrumentation()\n",[60,888,889],{"class":62,"line":87},[60,890,891],{},"        .AddHttpClientInstrumentation()\n",[60,893,894],{"class":62,"line":94},[60,895,896],{},"        .AddEntityFrameworkCoreInstrumentation()\n",[60,898,899],{"class":62,"line":100},[60,900,901],{},"        .AddJaegerExporter(j => j.AgentHost = \"jaeger\"));\n",[60,903,904],{"class":62,"line":106},[60,905,91],{"emptyLinePlaceholder":90},[60,907,908],{"class":62,"line":112},[60,909,910],{},"\u002F\u002F Custom span for a business operation:\n",[60,912,913],{"class":62,"line":118},[60,914,915],{},"private static readonly ActivitySource _src = new(\"MyApp.Orders\");\n",[60,917,918],{"class":62,"line":123},[60,919,91],{"emptyLinePlaceholder":90},[60,921,922],{"class":62,"line":129},[60,923,591],{},[60,925,926],{"class":62,"line":135},[60,927,72],{},[60,929,930],{"class":62,"line":141},[60,931,932],{},"    using var span = _src.StartActivity(\"PlaceOrder\");\n",[60,934,935],{"class":62,"line":147},[60,936,937],{},"    span?.SetTag(\"order.id\", order.Id);\n",[60,939,940],{"class":62,"line":153},[60,941,942],{},"    span?.SetTag(\"customer.id\", order.CustomerId);\n",[60,944,945],{"class":62,"line":159},[60,946,91],{"emptyLinePlaceholder":90},[60,948,949],{"class":62,"line":165},[60,950,951],{},"    try\n",[60,953,954],{"class":62,"line":170},[60,955,103],{},[60,957,958],{"class":62,"line":176},[60,959,960],{},"        var result = await _repo.SaveAsync(order);\n",[60,962,963],{"class":62,"line":182},[60,964,965],{},"        span?.SetStatus(ActivityStatusCode.Ok);\n",[60,967,968],{"class":62,"line":187},[60,969,970],{},"        return result;\n",[60,972,973],{"class":62,"line":193},[60,974,190],{},[60,976,978],{"class":62,"line":977},24,[60,979,980],{},"    catch (Exception ex)\n",[60,982,984],{"class":62,"line":983},25,[60,985,103],{},[60,987,989],{"class":62,"line":988},26,[60,990,991],{},"        span?.SetStatus(ActivityStatusCode.Error, ex.Message);\n",[60,993,995],{"class":62,"line":994},27,[60,996,997],{},"        span?.RecordException(ex);\n",[60,999,1001],{"class":62,"line":1000},28,[60,1002,1003],{},"        throw;\n",[60,1005,1007],{"class":62,"line":1006},29,[60,1008,190],{},[60,1010,1012],{"class":62,"line":1011},30,[60,1013,196],{},[10,1015,1017],{"id":1016},"performance-safe-logging","Performance-safe logging",[15,1019,1020,1021,1024],{},"Even disabled log levels have a cost if arguments are evaluated before the level\ncheck. Use ",[24,1022,1023],{},"[LoggerMessage]"," source generators for hot paths:",[51,1026,1028],{"className":53,"code":1027,"language":55,"meta":56,"style":56},"public partial class ItemProcessor\n{\n    [LoggerMessage(Level = LogLevel.Debug,\n                   Message = \"Processing item {Id} with payload {Payload}\")]\n    partial void LogProcessing(int id, string payload);\n\n    public void Process(Item item)\n    {\n        LogProcessing(item.Id, item.Payload); \u002F\u002F zero allocation if Debug is off\n    }\n}\n",[24,1029,1030,1035,1039,1044,1049,1054,1058,1063,1067,1072,1076],{"__ignoreMap":56},[60,1031,1032],{"class":62,"line":63},[60,1033,1034],{},"public partial class ItemProcessor\n",[60,1036,1037],{"class":62,"line":69},[60,1038,72],{},[60,1040,1041],{"class":62,"line":75},[60,1042,1043],{},"    [LoggerMessage(Level = LogLevel.Debug,\n",[60,1045,1046],{"class":62,"line":81},[60,1047,1048],{},"                   Message = \"Processing item {Id} with payload {Payload}\")]\n",[60,1050,1051],{"class":62,"line":87},[60,1052,1053],{},"    partial void LogProcessing(int id, string payload);\n",[60,1055,1056],{"class":62,"line":94},[60,1057,91],{"emptyLinePlaceholder":90},[60,1059,1060],{"class":62,"line":100},[60,1061,1062],{},"    public void Process(Item item)\n",[60,1064,1065],{"class":62,"line":106},[60,1066,103],{},[60,1068,1069],{"class":62,"line":112},[60,1070,1071],{},"        LogProcessing(item.Id, item.Payload); \u002F\u002F zero allocation if Debug is off\n",[60,1073,1074],{"class":62,"line":118},[60,1075,190],{},[60,1077,1078],{"class":62,"line":123},[60,1079,196],{},[15,1081,1082,1083,1086],{},"The source generator produces the same zero-allocation code as the manual\n",[24,1084,1085],{},"LoggerMessage.Define\u003CT>"," pattern but with a readable, type-safe call site.",[15,1088,1089,1092,1093,1095,1096,1099],{},[46,1090,1091],{},"Rule of thumb:"," Use ",[24,1094,1023],{}," for any logging in a method called more\nthan ~1000 times per second. For normal business logic paths, the standard\n",[24,1097,1098],{},"ILogger.LogXxx"," with message templates is fast enough.",[1101,1102,1103],"style",{},"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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":56,"searchDepth":69,"depth":69,"links":1105},[1106,1107,1108,1109,1110,1111,1112,1113,1114,1115],{"id":12,"depth":69,"text":13},{"id":34,"depth":69,"text":35},{"id":214,"depth":69,"text":215},{"id":360,"depth":69,"text":361},{"id":441,"depth":69,"text":442},{"id":573,"depth":69,"text":574},{"id":670,"depth":69,"text":671},{"id":758,"depth":69,"text":759},{"id":858,"depth":69,"text":859},{"id":1016,"depth":69,"text":1017},"Structured logging, health checks, and observability in ASP.NET Core — ILogger message templates, Serilog sinks, log scopes for correlation, OpenTelemetry metrics, and how to keep logging fast in hot paths.","medium","md",".NET Core","dotnet",{},"\u002Fblog\u002Fdotnet-logging-monitoring","\u002Fdotnet\u002Fperformance-deployment\u002Flogging-monitoring",{"title":5,"description":1116},"blog\u002Fdotnet-logging-monitoring","Logging & Monitoring","Performance & Deployment","performance-deployment","2026-06-23","l-a17e3S2NDFoccBaaOUAfVnNbp1tXj7zmjZ68ZdQ7I",1782244086006]