لاگ متمرکز برای برنامه‌های ⁦.NET Framework⁩ قدیمی، بدون مهاجرت

لاگ متمرکز برای برنامه‌های .NET Framework قدیمی، بدون مهاجرت
در این مقاله می‌خوانید
  1. چرا این بار مهاجرت لازم نیست
  2. نصب و پیکربندی در web.config
  3. راه‌اندازی در Global.asax
  4. خطاهایی که به Application_Error نمی‌رسند
  5. MVC با HandleErrorAttribute
  6. ⁦ASP.NET⁩ Web API 2
  7. WCF و Windows Service
  8. اگر log4net یا NLog دارید
  9. نکته‌های عملیاتی سرورهای قدیمی
  10. جمع‌بندی: یک بعدازظهر کار
  11. منابع و مطالعهٔ بیشتر

در بیشتر سازمان‌هایی که با آن‌ها کار کرده‌ام، دست‌کم یک سیستم مهم روی ⁦.NET Framework⁩ هست: یک پورتال ⁦ASP.NET⁩ MVC 5 که ده سال است کار می‌کند، چند صفحهٔ WebForms که کسی جرئت دست زدن به آن‌ها را ندارد، یک سرویس WCF که سه سیستم دیگر به آن وصل‌اند، و چند Windows Service که شب‌ها کارهای دسته‌ای را انجام می‌دهند. مهاجرت همهٔ این‌ها به ⁦.NET 8⁩ روی کاغذ برنامه‌ریزی شده و در عمل بودجه ندارد.

لاگ این سیستم‌ها معمولاً یکی از این سه حالت است: هیچ، فایل‌های log4net روی دیسک هر سرور، یا Event Viewer. و هر بار که مشکلی پیش می‌آید، کسی باید با Remote Desktop روی تک‌تک سرورها برود و فایل‌ها را بگردد. چرا این روش در مقیاس جواب نمی‌دهد را در مدیریت متمرکز لاگ توضیح داده‌ام. خبر خوب این مقاله این است که برای حل این مشکل، مهاجرت لازم نیست.

چرا این بار مهاجرت لازم نیست

Serilog 4 هم مستقیم برای ⁦.NET Framework 4.6.2⁩ و 4.7.1 ساخته شده و هم برای netstandard2.0. Serilog.Sinks.Seq روی netstandard2.0 است، پس روی هر برنامهٔ ⁦.NET Framework 4.6.2⁩ به بالا اجرا می‌شود. همین برای Serilog.Enrichers.Environment و Serilog.Settings.AppSettings هم صدق می‌کند. یعنی همان ابزاری که در Serilog در ⁦ASP.NET Core⁩ به‌کار بردم، با چند تفاوت در محل راه‌اندازی، اینجا هم کار می‌کند. تصویر کلی لاگ در دنیای ⁦.NET⁩ در راهنمای جامع لاگ در ⁦ASP.NET Core⁩ آمده است.

دو نکتهٔ واقع‌بینانه:

  • کتابخانه‌های netstandard2.0 روی 4.6.2 تا 4.7.1 کار می‌کنند ولی تعداد زیادی اسمبلی واسط (facade) و binding redirect با خودشان می‌آورند. روی 4.7.2 به بالا تجربه به‌مراتب روان‌تر است. اگر می‌توانید، retarget به 4.7.2 یا 4.8 کار کوچکی است و مهاجرت حساب نمی‌شود.
  • اگر پروژه هنوز روی 4.6.1 یا قدیمی‌تر است، بدانید که 4.5.2 و 4.6 و 4.6.1 از آوریل ۲۰۲۲ پشتیبانی رسمی ندارند و نسخه‌های قبل از آن هم خیلی زودتر. حداقل retarget به 4.6.2 لازم است؛ معمولاً تغییر یک خط در فایل پروژه و یک دور تست.

نصب و پیکربندی در web.config

در Package Manager Console:

Install-Package Serilog
Install-Package Serilog.Sinks.Seq
Install-Package Serilog.Enrichers.Environment
Install-Package Serilog.Settings.AppSettings

پکیج آخر اجازه می‌دهد پیکربندی را در appSettings فایل web.config (یا app.config برای سرویس‌ها) بگذارید. این برای سیستم‌های قدیمی مزیت جدی است: تیم عملیات بدون کامپایل دوباره سطح لاگ را عوض می‌کند، و web.config transform که احتمالاً همین حالا برای محیط‌ها دارید، نشانی و کلید را هم جدا می‌کند:

<appSettings>
  <add key="serilog:minimum-level" value="Information" />
  <add key="serilog:using:Seq" value="Serilog.Sinks.Seq" />
  <add key="serilog:write-to:Seq.serverUrl" value="https://ingest.logmug.ir" />
  <add key="serilog:write-to:Seq.apiKey" value="lm_ingest_..." />
  <add key="serilog:write-to:Seq.bufferBaseFilename" value="C:\Logs\portal\buffer" />
  <add key="serilog:enrich:with-property:Application" value="legacy-portal" />
  <add key="serilog:enrich:with-property:Environment" value="Production" />
</appSettings>

bufferBaseFilename اختیاری است ولی روی سرورهایی که ارتباط بیرونی‌شان همیشه پایدار نیست توصیه‌اش می‌کنم: رویدادها اول روی دیسک نوشته و بعد فرستاده می‌شوند، پس قطعی چنددقیقه‌ای یا ری‌استارت application pool چیزی را از بین نمی‌برد. هویت application pool باید اجازهٔ نوشتن در آن پوشه را داشته باشد.

یک قید از مستندات Serilog.Settings.AppSettings: هر sink یا باید کاملاً در XML تعریف شود یا کاملاً در کد. ترکیب این دو برای یک sink کار نمی‌کند. enricherها را می‌توانید بی‌مشکل در کد اضافه کنید.

راه‌اندازی در Global.asax

برای MVC و WebForms، جای طبیعی Application_Start است:

using System;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using Serilog;

public class MvcApplication : HttpApplication
{
    protected void Application_Start()
    {
        Log.Logger = new LoggerConfiguration()
            .ReadFrom.AppSettings()
            .Enrich.FromLogContext()
            .Enrich.WithMachineName()
            .CreateLogger();

        AppDomain.CurrentDomain.UnhandledException += (sender, e) =>
            Log.Fatal(e.ExceptionObject as Exception,
                "Unhandled exception in AppDomain, terminating: {IsTerminating}", e.IsTerminating);

        AreaRegistration.RegisterAllAreas();
        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
        RouteConfig.RegisterRoutes(RouteTable.Routes);

        Log.Information("Application started");
    }

    protected void Application_Error(object sender, EventArgs e)
    {
        var ex = Server.GetLastError();
        var http = ex as HttpException;
        if (http != null && http.GetHttpCode() == 404)
            return;

        Log.Error(ex, "Unhandled exception on {HttpMethod} {Path}",
            Request.HttpMethod, Request.Path);
    }

    protected void Application_End()
    {
        Log.Information("Application stopping");
        Log.CloseAndFlush();
    }
}

چند توضیح:

  • Request.Path، نه Request.RawUrl. query string اغلب توکن یا شناسهٔ نشست دارد و جایش در لاگ نیست.
  • ۴۰۴ را جدا کنید. ربات‌ها روزانه هزاران نشانی بی‌ربط را امتحان می‌کنند؛ اگر این‌ها Error باشند، خطاهای واقعی زیرشان گم می‌شوند.
  • CloseAndFlush در Application_End. IIS application pool را دوره‌ای بازیافت می‌کند؛ بدون این خط، رویدادهای چند ثانیهٔ آخر هر بازیافت از دست می‌روند.

در کلاس‌ها لاگر را با Log.ForContext<OrderService>() بگیرید تا SourceContext داشته باشید. فقط یک دام: اگر این را در فیلد static readonly کلاسی بگذارید که پیش از Application_Start لمس شود، لاگر خاموش پیش‌فرض را برای همیشه نگه می‌دارد و آن کلاس هیچ لاگی نمی‌فرستد.

خطاهایی که به Application_Error نمی‌رسند

اینجا بیشتر تیم‌ها غافلگیر می‌شوند: Application_Error همهٔ خطاها را نمی‌بیند.

MVC با HandleErrorAttribute

اگر در FilterConfig فیلتر HandleErrorAttribute دارید و customErrors روشن است، خطا همان‌جا «مدیریت‌شده» علامت می‌خورد و به Application_Error نمی‌رسد. کاربر صفحهٔ خطای زیبا می‌بیند و شما هیچ لاگی ندارید. یک فیلتر کوچک کنارش اضافه کنید:

public class SerilogExceptionFilter : IExceptionFilter
{
    public void OnException(ExceptionContext filterContext)
    {
        Log.Error(filterContext.Exception, "MVC action failed: {Controller}.{Action}",
            filterContext.RouteData.Values["controller"],
            filterContext.RouteData.Values["action"]);
    }
}

// در FilterConfig.RegisterGlobalFilters
filters.Add(new SerilogExceptionFilter());

اگر HandleErrorAttribute ندارید، این فیلتر لازم نیست و Application_Error کافی است؛ وگرنه هر خطا دو بار ثبت می‌شود.

⁦ASP.NET⁩ Web API 2

Web API خطاها را در pipeline خودش به پاسخ ۵۰۰ تبدیل می‌کند و Application_Error هرگز آن‌ها را نمی‌بیند. نقطهٔ درست، ExceptionLogger است:

public class SerilogExceptionLogger : ExceptionLogger
{
    public override void Log(ExceptionLoggerContext context)
    {
        Serilog.Log.Error(context.Exception, "Web API request failed: {Method} {Path}",
            context.Request.Method, context.Request.RequestUri.AbsolutePath);
    }
}

// در WebApiConfig.Register
config.Services.Add(typeof(IExceptionLogger), new SerilogExceptionLogger());

نام کامل Serilog.Log عمدی است، چون خود متد هم Log نام دارد.

WCF و Windows Service

در WCF، خطاهای سرویس با IErrorHandler گرفته می‌شوند: در متد HandleError لاگ کنید و آن را با یک service behavior به سرویس وصل کنید. در Windows Service و برنامه‌های کنسولی، لاگر را در OnStart بسازید و هر دو رخداد سراسری را بگیرید:

protected override void OnStart(string[] args)
{
    Log.Logger = new LoggerConfiguration()
        .ReadFrom.AppSettings()
        .Enrich.WithMachineName()
        .CreateLogger();

    AppDomain.CurrentDomain.UnhandledException += (s, e) =>
    {
        Log.Fatal(e.ExceptionObject as Exception, "Service crashed");
        Log.CloseAndFlush();
    };
    TaskScheduler.UnobservedTaskException += (s, e) =>
        Log.Error(e.Exception, "Unobserved task exception");

    // شروع کار اصلی سرویس
}

protected override void OnStop()
{
    Log.Information("Service stopping");
    Log.CloseAndFlush();
}

CloseAndFlush داخل handler خطای مدیریت‌نشده مهم است: فرایند چند لحظه بعد می‌میرد و این تنها فرصت فرستادن همان رویدادی است که علت مرگ را می‌گوید.

اگر log4net یا NLog دارید

هزاران فراخوانی log.Info(...) را بازنویسی نکنید. هر دو کتابخانه راه اتصال به مقصد سازگار با Seq دارند:

  • NLog — پکیج NLog.Targets.Seq (از سازندگان Seq) یک target اضافه می‌کند. NLog از نسخهٔ 4.5 قالب پیام نام‌دار را پشتیبانی می‌کند، پس فراخوانی‌هایی مثل logger.Info("Order {OrderId} placed", id) ویژگی واقعی می‌سازند.
  • log4net، راه مستقیم — پکیج Seq.Client.Log4Net یک appender است که از 4.6.2 پشتیبانی می‌کند. خود سازندگانش می‌گویند در حالت نگه‌داری است و appender به‌طور پیش‌فرض همزمان کار می‌کند؛ برای پروداکشن bufferSize را دست‌کم ۱۰۰ بگذارید و آن را با Log4Net.Async بپیچید.
  • log4net، راه پل — پکیج جامعه‌محور Log4net.Appender.Serilog رویدادهای log4net را به pipeline خود Serilog می‌فرستد. کد قدیمی دست نمی‌خورد و کد جدید مستقیم با Serilog و قالب پیام نوشته می‌شود؛ یک مسیر تدریجی واقعی.

نمونهٔ NLog.config:

<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <extensions>
    <add assembly="NLog.Targets.Seq" />
  </extensions>
  <targets>
    <target name="seq" xsi:type="BufferingWrapper" bufferSize="1000" flushTimeout="2000">
      <target xsi:type="Seq" serverUrl="https://ingest.logmug.ir" apiKey="lm_ingest_...">
        <property name="Application" value="billing-service" />
        <property name="MachineName" value="${machinename}" />
      </target>
    </target>
  </targets>
  <rules>
    <logger name="*" minlevel="Info" writeTo="seq" />
  </rules>
</nlog>

هر دو کلاینت، در کد منبعشان، همان مسیر و همان قالب CLEF را به‌کار می‌برند که Serilog.Sinks.Seq. با این حال، پیش از پروداکشن با یک رویداد آزمایشی مطمئن شوید که سطح، پیام و ویژگی‌ها همان‌طور که انتظار دارید می‌رسند. و انتظار واقع‌بینانه داشته باشید: پیام‌های قدیمی log4net با {0} و InfoFormat نوشته شده‌اند و مقدارهایشان فقط در متن است. متمرکز شدن را امروز می‌گیرید؛ مزیت لاگ ساخت‌یافته را به‌تدریج و با کد جدید.

نکته‌های عملیاتی سرورهای قدیمی

  • TLS. اگر در web.config، <httpRuntime targetFramework="4.5" /> یا قدیمی‌تر دارید، برنامه ممکن است با پیش‌فرض‌های قدیمی پروتکل امنیتی اجرا شود و اتصال HTTPS به سرویس‌های امروزی شکست بخورد. توصیهٔ مایکروسافت هدف‌گیری 4.7 به بالا و سپردن انتخاب پروتکل به سیستم‌عامل است؛ تغییر targetFramework رفتارهای دیگری را هم عوض می‌کند، پس تست کنید. اگر روی 4.6.2 گیر کرده‌اید، راه موقت ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; در ابتدای Application_Start است.
  • پروکسی و فایروال. سرورهای داخلی اغلب دسترسی بیرونی ندارند. یا پورت ۴۴۳ را فقط برای نشانی مقصد لاگ باز کنید، یا از پارامتر messageHandler در WriteTo.Seq با یک HttpClientHandler که پروکسی را می‌شناسد استفاده کنید.
  • وقتی لاگ‌ها نمی‌رسند. Serilog خطاهای خودش را بی‌صدا می‌بلعد تا برنامه را از کار نیندازد. برای عیب‌یابی، موقتاً Serilog.Debugging.SelfLog.Enable(msg => System.Diagnostics.Trace.WriteLine(msg)); را روشن کنید؛ کلید اشتباه یا خطای TLS همان‌جا دیده می‌شود.
  • لاگ هر درخواست. پکیج SerilogWeb.Classic برای System.Web همان کاری را می‌کند که UseSerilogRequestLogging در ⁦ASP.NET Core⁩. فقط بدانید آخرین نسخه‌اش مال ۲۰۲۱ است؛ پیش از استفاده با Serilog 4 در محیط تست امتحانش کنید.

جمع‌بندی: یک بعدازظهر کار

  1. اطمینان از اینکه پروژه دست‌کم روی 4.6.2 است (ترجیحاً 4.7.2 یا 4.8).
  2. نصب چهار پکیج و افزودن کلیدهای serilog: به web.config.
  3. ساخت لاگر در Application_Start و CloseAndFlush در Application_End.
  4. گرفتن خطاها در همهٔ نقطه‌ها: Application_Error، فیلتر MVC یا ExceptionLogger Web API، و AppDomain.UnhandledException.
  5. یک throw عمدی در محیط تست و دیدن آن در مقصد، با stack trace کامل و نام سرور.

اگر مقصدتان LogMug است، راهنمای اتصال برنامه‌های ⁦.NET Framework⁩ همین مراحل را با نشانی و کلید واقعی پروژه‌تان قدم‌به‌قدم دارد. ولی با هر مقصدی، از لحظه‌ای که لاگ این سیستم‌های قدیمی کنار بقیه بنشیند، دیگر لازم نیست برای پیدا کردن یک خطا با Remote Desktop سراغ پنج سرور بروید؛ و این، بیش از هر مهاجرتی، شب‌های کشیک را آرام‌تر می‌کند. قدم بعدی، روش پیدا کردن علت با همین لاگ‌هاست که در عیب‌یابی خطا در پروداکشن آورده‌ام.

منابع و مطالعهٔ بیشتر

لاگ همهٔ سرویس‌هایتان را در یک جا جستجو کنید

C#، Java یا هر زبان دیگر — با چند خط پیکربندی وصل می‌شود. پلن رایگان کارت بانکی نمی‌خواهد.

شروع رایگان