مقالات › ASP.NET Core

لاگ‌گیری ساخت‌یافته در ASP.NET Core با Serilog

لاگ‌گیری در سطح Production را در ASP.NET Core راه بیندازید: مبانی ILogger، سطوح لاگ، لاگ ساخت‌یافته، Sinkهای Serilog، لاگ درخواست‌ها، Enricher، Correlation ID و چیزهایی که هرگز نباید لاگ شوند.

وقتی چیزی در Production خراب می‌شود، لاگ‌ها اغلب تنها شاهد ماجرا هستند. لاگ خوب در چند دقیقه جواب می‌دهد چه اتفاقی افتاد، برای چه کسی، کِی و چرا؛ لاگ بد یا ساکت است یا دیواری از نویز. این راهنما ILogger داخلی، لاگ ساخت‌یافته و یک پیکربندی آماده‌ی Production با Serilog را برای ASP.NET Core پوشش می‌دهد.

مبانی ILogger

ASP.NET Core یک انتزاع لاگ‌گیری دارد که هر جا بخواهید تزریق می‌شود:

public class OrderService(ILogger<OrderService> logger)
{
    public async Task PlaceAsync(Order order)
    {
        logger.LogInformation("Placing order {OrderId} for customer {CustomerId}", order.Id, order.CustomerId);
        // ...
    }
}

نوع جنریک (OrderService) دسته (Category) لاگ می‌شود؛ پس می‌توانید لاگ‌ها را بر اساس کلاس یا Namespace فیلتر کنید.

سطوح لاگ

سطح کاربرد مثال
Trace جزئیات تشخیصی بسیار ریز Payload خام (هرگز در Production)
Debug تشخیص برای توسعه‌دهنده کش برای کلید X پیدا نشد
Information رویدادهای عادی بیزینس سفارش ثبت شد، کاربر ثبت‌نام کرد
Warning غیرمنتظره اما مدیریت‌شده تلاش مجدد درگاه پرداخت، کوئری کند
Error شکست عملیات فعلی پرداخت با Exception شکست خورد
Critical برنامه نمی‌تواند ادامه دهد دیتابیس در شروع برنامه در دسترس نیست

حداقل سطح را برای هر دسته در appsettings.json تنظیم کنید:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.EntityFrameworkCore.Database.Command": "Warning"
    }
  }
}

لاگ ساخت‌یافته: قالب پیام، نه چسباندن رشته

// ❌ Interpolation رشته: مقادیر داخل یک رشته‌ی ساده حل می‌شوند
logger.LogInformation($"Order {order.Id} total {order.Total}");

// ✅ قالب پیام: مقادیر به‌صورت ویژگی‌های نام‌دار ثبت می‌شوند
logger.LogInformation("Order {OrderId} total {Total}", order.Id, order.Total);

با یک Sink ساخت‌یافته، حالت دوم OrderId و Total را به‌صورت فیلدهای جدا ذخیره می‌کند. آن‌وقت می‌توانید جستجو کنید «همه‌ی خطاهایی که OrderId = 4821» یا «میانگین Total سفارش‌های ناموفق»؛ کاری که با متن ساده ممکن نیست. قالب‌ها سریع‌تر هم هستند، چون اگر سطح لاگ غیرفعال باشد قالب‌بندی انجام نمی‌شود.

لاگ پرسرعت با Source Generator

برای مسیرهای داغ، متدهای لاگ Strongly-Typed را در زمان کامپایل تولید کنید:

public static partial class Log
{
    [LoggerMessage(EventId = 1001, Level = LogLevel.Information,
        Message = "Article {Slug} viewed in {Language}")]
    public static partial void ArticleViewed(ILogger logger, string slug, string language);
}

Log.ArticleViewed(logger, slug, lang);

چرا Serilog؟

Providerهای داخلی در Console و خروجی Debug می‌نویسند. Serilog مقصدهای (Sink) متنوع (فایل، Seq، Elasticsearch، Application Insights و...)، Enricherها و یک مدل داده‌ی ساخت‌یافته‌ی عالی اضافه می‌کند و همچنان به ILogger متصل می‌شود.

dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.File
dotnet add package Serilog.Sinks.Seq

راه‌اندازی Serilog

using Serilog;

Log.Logger = new LoggerConfiguration()
    .WriteTo.Console()
    .CreateBootstrapLogger();   // خطاهای زمان شروع را هم ثبت می‌کند

try
{
    var builder = WebApplication.CreateBuilder(args);

    builder.Host.UseSerilog((context, services, config) => config
        .ReadFrom.Configuration(context.Configuration)
        .ReadFrom.Services(services)
        .Enrich.FromLogContext()
        .Enrich.WithProperty("Application", "Portfolio.Api"));

    var app = builder.Build();
    app.UseSerilogRequestLogging();   // یک خط تمیز برای هر درخواست
    // ...
    app.Run();
}
catch (Exception ex)
{
    Log.Fatal(ex, "Application terminated unexpectedly");
}
finally
{
    Log.CloseAndFlush();
}

پیکربندی در appsettings.json:

{
  "Serilog": {
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft.AspNetCore": "Warning",
        "Microsoft.EntityFrameworkCore": "Warning"
      }
    },
    "WriteTo": [
      { "Name": "Console" },
      {
        "Name": "File",
        "Args": {
          "path": "logs/app-.log",
          "rollingInterval": "Day",
          "retainedFileCountLimit": 14,
          "formatter": "Serilog.Formatting.Compact.CompactJsonFormatter, Serilog.Formatting.Compact"
        }
      },
      { "Name": "Seq", "Args": { "serverUrl": "http://localhost:5341" } }
    ]
  }
}

فایل‌های چرخشی (Rolling) مصرف دیسک را محدود نگه می‌دارند؛ قالب JSON لاگ‌ها را برای ماشین خوانا می‌کند و Seq یک رابط رایگان محلی برای جستجوی لاگ‌های ساخت‌یافته در اختیارتان می‌گذارد.

لاگ درخواست‌ها

UseSerilogRequestLogging() جای خطوط متعدد ASP.NET Core برای هر درخواست را با یک خط خلاصه می‌گیرد:

HTTP GET /fa/articles/csharp-async-await-guide responded 200 in 18.3 ms

ویژگی‌های مفیدی به آن خط اضافه کنید:

app.UseSerilogRequestLogging(options =>
{
    options.EnrichDiagnosticContext = (diag, http) =>
    {
        diag.Set("UserId", http.User.FindFirst("sub")?.Value ?? "anonymous");
        diag.Set("ClientIp", http.Connection.RemoteIpAddress?.ToString());
    };
});

غنی‌سازی (Enrichment) و Correlation ID

با Scope به همه‌ی لاگ‌هایی که در طول یک عملیات نوشته می‌شوند زمینه اضافه کنید:

using (logger.BeginScope(new Dictionary<string, object>
{
    ["OrderId"] = order.Id,
    ["CorrelationId"] = correlationId
}))
{
    logger.LogInformation("Charging payment");
    logger.LogInformation("Sending confirmation");   // هر دو خط OrderId و CorrelationId دارند
}

یک Correlation ID که بین سرویس‌ها منتقل می‌شود (از طریق هدر X-Correlation-ID) اجازه می‌دهد یک اقدام کاربر را در API، Workerهای پس‌زمینه و سرویس‌های دیگر دنبال کنید.

لاگ درست Exceptionها

try
{
    await gateway.ChargeAsync(order);
}
catch (PaymentException ex)
{
    // ✅ Exception را آرگومان اول بدهید تا Stack Trace حفظ شود
    logger.LogError(ex, "Payment failed for order {OrderId}", order.Id);
    throw;
}

هر Exception را یک بار لاگ کنید؛ ترجیحاً جایی که مدیریت می‌شود (اغلب Handler سراسری خطا). لاگ و پرتاب مجدد در هر لایه نویز تکراری می‌سازد.

چیزهایی که هرگز نباید لاگ شوند

  • رمز عبور، توکن، کلید API و کوکی نشست
  • شماره کارت کامل، کد ملی و داده‌های شخصی حساس
  • کل بدنه‌ی درخواست و پاسخ به‌طور پیش‌فرض

اگر احتمال عبور فیلدهای حساس وجود دارد، از سیاست‌های Destructuring یا Enricherهای ماسک‌کننده‌ی Serilog استفاده کنید و یادتان باشد قوانین حریم خصوصی شامل لاگ‌ها هم می‌شود.

نکات کارایی

  • داخل حلقه‌های فشرده در سطح Information لاگ ننویسید.
  • قبل از ساخت داده‌ی Debug پرهزینه، IsEnabled(LogLevel.Debug) را بررسی کنید.
  • Sinkهای ناهمگام/دسته‌ای را ترجیح دهید (Sinkهای Seq و Elasticsearch به‌طور پیش‌فرض دسته‌ای می‌فرستند).
  • EnableSensitiveDataLogging در EF Core را فقط در محیط توسعه فعال کنید.

جمع‌بندی

ILogger<T> را همه‌جا تزریق کنید، سطوح را آگاهانه انتخاب کنید و همیشه از قالب پیام استفاده کنید تا لاگ‌ها ساخت‌یافته باشند. Serilog را برای فایل‌های چرخشی، خروجی JSON و یک رابط جستجوپذیر مثل Seq اضافه کنید، لاگ درخواست‌ها را فعال کنید، لاگ‌ها را با Scope و Correlation ID غنی کنید، هر Exception را یک بار همراه با Stack Trace ثبت کنید و هرگز اطلاعات محرمانه را لاگ نکنید. خودِ آینده‌تان — که ساعت دو بامداد در حال دیباگ است — از شما ممنون خواهد بود.

پرسش‌های متداول

Serilog یا لاگ داخلی؟

انتزاع داخلی برای برنامه‌های ساده کافی است. Serilog برای سیستم‌های Production مقصدها، Enricherها و خروجی ساخت‌یافته‌ی بهتری اضافه می‌کند و همچنان با ILogger کد می‌نویسید.

تفاوت لاگ، متریک و Trace چیست؟

لاگ رویدادهای مجزا را ثبت می‌کند؛ متریک اندازه‌گیری عددی در طول زمان است (درخواست در ثانیه)؛ Trace یک درخواست را در سرویس‌های مختلف دنبال می‌کند. OpenTelemetry هر سه را یکپارچه می‌کند.

لاگ‌ها را تا چه مدت نگه داریم؟

به اندازه‌ای که برای بررسی حوادث و الزامات قانونی کافی باشد؛ معمولاً ۱۴ تا ۹۰ روز برای لاگ‌های برنامه و مدت بیشتر برای لاگ‌های ممیزی.