لاگینگ در NET. - بهترین شیوهها 📝
لاگها باید به شما در رفع باگها کمک کنند و توضیح دهند که اپلیکیشن در حال حاضر چه کاری انجام میدهد. آنها نباید شما را در متن غرق کنند یا آن یک خطی را که نیاز دارید، پنهان کنند. این راهنما نشان میدهد که چگونه لاگینگ را در NET. مدرن راهاندازی کنید، پیامهای ساختاریافته (structured) بنویسید، سطح مناسب را انتخاب کنید و request correlation را اضافه کنید. همچنین، لاگها را با متریکها اشتباه نگیرید، اشتباه است که برای بررسی تعداد فراخوانیهای API در هر بازه زمانی X، لاگ اضافه کنید.
شروع سریع (Minimal API) 🚀
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// لاگینگ در کنسول به صورت پیشفرض فعال است؛ میتوانید آن را از طریق appsettings.json تغییر دهید
var app = builder.Build();
app.MapGet("/", (ILogger<Program> log) =>
{
log.LogInformation("Health probe hit at {UtcNow}", DateTime.UtcNow);
return "OK";
});
app.Run();
این کد از قبل در کنسول با timestamp، سطح لاگ، دستهبندی و پیام مینویسد. پیکربندی
در appsettings.json قرار دارد.
سطوح لاگ که منطقی هستند
🔬 Trace :
بسیار پرحرف، جریان گامبهگام. در پروداکشن خاموش است.
🐞 Debug :
در طول توسعه مفید است؛ بعداً میتوان آن را با خیال راحت غیرفعال کرد.
ℹ️ Information :
رویدادهای سطح بالا: شروع برنامه، تکمیل درخواست، رویدادهای بیزینسی.
⚠️ Warning :
شرایط غیرعادی: تلاشهای مجدد (retries)، تایماوتهایی که بازیابی میشوند.
❌ Error :
شکستهایی که شما catch و مدیریت میکنید (شامل exception).
🔥 Critical :
اپلیکیشن سالم نیست.
پایینترین سطحی را انتخاب کنید که هنوز فوریت مناسب را منتقل میکند. اگر همه چیز Information باشد، هیچ چیز Information نیست.
لاگینگ ساختاریافته بهتر از الحاق رشته است
قالبهای پیام (Message templates)، دادهها را برای کوئری زدن در آینده در فیلدها نگه میدارند. در اینجا از درونیابی رشته (string interpolation) خودداری کنید.
// ✅ خوب
log.LogInformation("User {UserId} logged in from {Ip}", userId, ip);
// ❌ بد (کوئری زدن سخت است)
log.LogInformation($"User {userId} logged in from {ip}");
با قالبها، ابزارها میتوانند بر روی UserId یا Ip بدون تجزیه متن، فیلتر کنند.
استثناها را به عنوان اولین آرگومان وارد کنید:
try
{
await service.ProcessAsync(orderId);
}
catch (Exception ex)
{
log.LogError(ex, "Failed to process order {OrderId}", orderId);
}
پیکربندی لاگینگ از طریق appsettings.json ⚙️
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning"
},
"Console": {
"FormatterName": "json",
"FormatterOptions": {
"IncludeScopes": true,
"UseUtcTimestamp": true
}
}
}
}دستهبندیهای پرسروصدا (مانند جزئیات داخلی فریمورک) را به Warning کاهش دهید.
فرمتدهنده JSON با جمعآوریکنندههای لاگ به خوبی کار میکند و فیلدها را ساختاریافته نگه میدارد.
دستهبندیها و DI🗃
دستهبندی لاگر، نوعی است که شما درخواست میکنید. این به فیلتر کردن و گروهبندی لاگها کمک میکند.
public sealed class BillingService(ILogger<BillingService> log)
{
public void Charge(Guid orderId, decimal amount)
=> log.LogInformation("Charging {OrderId} {Amount}", orderId, amount);
}
Scopeها و Correlation IDها 🔗
اسکوپ ها پراپرتیهای اضافی را به هر خط لاگ داخل یک بلوک متصل میکنند. یک Correlation ID برای هر درخواست اضافه کنید تا بتوانید یک مسیر را در سراسر ماژولها ردیابی کنید.
app.Use(async (ctx, next) =>
{
var cid = ... // Get Correlation ID from header or create a new one
using (loggerFactory.CreateLogger("Correlation").BeginScope(new Dictionary<string, object?>
{
["CorrelationId"] = cid
}))
{
// ...
await next();
}
});
حالا هر خط لاگ در آن درخواست شامل CorrelationId است.