لاگ ساختاریافته و شناسه همبستگی در ASP.NET Core: دیباگ production بدون حدس زدن

پاسخ کوتاه: رویداد با property لاگ کنید، نه جمله. از Serilog (یا خود ILogger) با message template استفاده کنید، لاگ را JSON بنویسید تا قابل جستجو باشد، به هر درخواست یک شناسه همبستگی (correlation id) بدهید، آن را در همه خطوط لاگ بگذارید و در پاسخ برگردانید تا کاربر یا فرانت‌اند بتواند گزارشش کند. آن‌وقت دیباگ یک مشکل production با یک کوئری شروع می‌شود، نه یک بعدازظهر اسکرول.

یک مثال واقعی: «پرداخت حدود ساعت سه خطا داد»

پشتیبانی شکایتی را می‌فرستد: پرداختی حدود ساعت ۳ بعدازظهر ناموفق شده. فایل لاگ بین ۲:۵۰ تا ۳:۱۰ چهل هزار خط دارد، همه متن ساده مثل Payment failed for user 5521. شناسه کاربر را grep می‌کنید، سه خطا پیدا می‌کنید و نمی‌توانید بفهمید کدام مال این درخواست است یا قبلش در کلاینت درگاه چه اتفاقی افتاده.

با لاگ ساختاریافته، صفحه خطا یک کد کوتاه به کاربر نشان داده: شناسه همبستگی. یک فیلتر CorrelationId = '7f3c...' خط درخواست، فراخوانی درگاه، retry، timeout و وضعیت نهایی را به ترتیب برمی‌گرداند. پنج دقیقه به‌جای دو ساعت.

راه‌اندازی Serilog

پکیج‌ها: Serilog.AspNetCore و Serilog.Formatting.Compact.

using Serilog;
using Serilog.Formatting.Compact;

builder.Host.UseSerilog((ctx, services, cfg) => cfg
    .ReadFrom.Configuration(ctx.Configuration)
    .ReadFrom.Services(services)
    .Enrich.FromLogContext()
    .WriteTo.Console()
    .WriteTo.File(new CompactJsonFormatter(), "logs/app-.json",
        rollingInterval: RollingInterval.Day,
        retainedFileCountLimit: 14));

شناسه همبستگی به‌علاوه لاگ درخواست:

using System.Diagnostics;
using Serilog.Context;

var app = builder.Build();

// 1. Correlation first, so every later log line carries it
app.Use(async (ctx, next) =>
{
    var correlationId = ctx.Request.Headers["X-Correlation-ID"].FirstOrDefault()
        ?? Activity.Current?.TraceId.ToString()
        ?? ctx.TraceIdentifier;

    ctx.Response.Headers["X-Correlation-ID"] = correlationId;

    using (LogContext.PushProperty("CorrelationId", correlationId))
    {
        await next();
    }
});

// 2. One summary line per request: method, path, status, elapsed ms
app.UseSerilogRequestLogging();

ترتیب مهم است: middleware همبستگی قبل از UseSerilogRequestLogging اجرا می‌شود تا خط خلاصه درخواست هم شناسه را داشته باشد.

template بنویسید، نه رشته

// Bad: one unique string per call, nothing to filter on
log.LogInformation($"Withdraw {amount} from {walletId} failed");

// Good: a template plus properties you can query
log.LogWarning("Withdraw of {AmountRials} from {WalletId} failed: {Reason}",
    amount, walletId, error.Code);

نسخه template، فیلدهای AmountRials و WalletId و Reason را جدا ذخیره می‌کند؛ پس می‌توانید بپرسید «همه برداشت‌های ناموفق بالای ۵۰ میلیون ریال امروز» به‌جای جستجوی متن.

نکات عملی

  • شناسه را به کاربر نشان دهید. شناسه همبستگی را در صفحه خطا و پاسخ‌های ProblemDetails بگذارید. کاربر آن را در تیکت پشتیبانی کپی می‌کند و شما دقیقاً همان درخواست را دارید.
  • بین سرویس‌ها، HttpClient هدر W3C traceparent را خودش منتقل می‌کند، پس Activity.TraceId زنجیره فراخوانی را دنبال می‌کند. هدر سفارشی را برای کلاینت‌هایی نگه دارید که traceparent نمی‌فرستند.
  • روی IIS مطمئن شوید هویت app pool اجازه نوشتن در پوشه logs را دارد و پوشه را بیرون از wwwroot بگذارید. یک نسخه رایگان Seq روی همان سرور، لاگ JSON را در مرورگر قابل جستجو می‌کند.
  • سطح‌ها: Information برای رویدادهای کسب‌وکار، Warning برای شکست‌های مدیریت‌شده، Error برای چیزی که کسی باید بررسی کند. اگر همه خطوط Error باشند، هیچ‌کدام نیست.

مزایا و معایب

مزایا

  • حادثه‌ها با کوئری بررسی می‌شوند، نه با حدس.
  • یک شناسه، گزارش کاربر، درخواست و همه فراخوانی‌های پایین‌دستی را به هم وصل می‌کند.
  • همین لاگ‌ها بعداً بدون تغییر کد داشبورد و هشدار را تغذیه می‌کنند.

معایب

  • لاگ JSON خام سخت‌تر خوانده می‌شود؛ یک نمایشگر مثل Seq لازم دارید.
  • property بیشتر یعنی فضای بیشتر؛ نگهداری و سطح لاگ را آگاهانه تنظیم کنید.
  • لاگ کردن تصادفی داده حساس راحت‌تر می‌شود؛ خطوط لاگ جدید را review کنید.

مشکلات production که توضیحشان روزها طول می‌کشد؟ ببینید چطور می‌توانم کمک کنم.

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

چرا فقط از ILogger بدون Serilog استفاده نکنم؟

می‌شود: ILogger خودش message template و scope دارد. Serilog مقصدها (فایل JSON چرخشی، Seq، Elasticsearch)، enricherها و لاگ درخواست را اضافه می‌کند که ساختنشان خسته‌کننده است. کد شما در هر حال از ILogger لاگ می‌نویسد.

TraceId کافی است یا هدر همبستگی خودم را لازم دارم؟

بین سرویس‌های دات‌نتی Activity.TraceId با هدر W3C traceparent خودکار منتقل می‌شود، پس TraceId کافی است. هدر سفارشی X-Correlation-ID وقتی مفید است که فراخوان اپ موبایل یا شریکی است که traceparent نمی‌فرستد.

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

شماره کارت، CVV، رمز، کد یک‌بارمصرف، توکن، کد ملی کامل و بدنه کامل درخواست‌های پرداخت. به‌جایش شناسه و مقدار ماسک‌شده لاگ کنید و خطوط لاگ جدید را در code review از این نظر بررسی کنید.