将 Serilog 与 WebApi2 一起使用的正确方法

Correct way to use Serilog with WebApi2

我正在寻找将 serilog 与 aspnet webapi2 一起使用的正确方法。 至于现在我像这样初始化全局 Log.Logger 属性 :

   public static void Register(HttpConfiguration config)
    {

        Log.Logger = new LoggerConfiguration()
            .WriteTo.Elasticsearch(new ElasticsearchSinkOptions(new Uri("http://localhost:9200"))
            {
                IndexFormat = IndexFormat,
                BufferBaseFilename = outputLogPath,
                AutoRegisterTemplate = true,
                AutoRegisterTemplateVersion = AutoRegisterTemplateVersion.ESv6,
                CustomFormatter = new ElasticsearchJsonFormatter(renderMessageTemplate: false),
                BufferFileCountLimit = NbDaysRetention
            })
            .MinimumLevel.ControlledBy(new LoggingLevelSwitch() { MinimumLevel = LogEventLevel.Information})
            .Enrich.FromLogContext()
            .Enrich.WithWebApiRouteTemplate()
            .Enrich.WithWebApiActionName()
            .CreateLogger();

        //Trace all requests
        SerilogWebClassic.Configure(cfg => cfg.LogAtLevel(LogEventLevel.Information));


        config.MapHttpAttributeRoutes();

        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
    }

有没有更简洁的方法呢?我想知道,如果我必须为我的控制器进行一些测试,这是否会成为一个问题。

对于带有 Web API (and/or MVC) 的应用程序,我经常使用以下代码组织。 (请注意,它可能有点过时,因为它基于某些软件包的旧版本,但你应该能够在不做太多工作的情况下适应它......)

您将需要相当多的包,您应该能够从命名空间中猜出这些包,但最重要的是,安装 WebActivatorEx 包,它提供了一种在不同位置使用代码 运行 的方法应用程序生命周期的时刻

我们的 Global.asax.cs 最终看起来像这样:

    public class WebApiApplication : System.Web.HttpApplication
    {
        // rely on the fact that AppPreStart is called before Application_Start
        private static readonly ILogger Logger = Log.ForContext<WebApiApplication>();

        public override void Init()
        {
            base.Init();
        }

        protected void Application_Start()
        {
            // WARNING :  Some code runs even before this method ... see AppPreStart

            Logger.Debug("In Application_Start");
            // Mvc (must be before)
            AreaRegistration.RegisterAllAreas(); 
            // Web API
            // ... snip ...     
            // some DependencyInjection config ...
            // ... snip ...  
            // MVC
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);

            Logger.Information("App started !");
        }


        protected void Application_End(object sender, EventArgs e)
        {
            Logger.Debug("In Application_End");
            ApplicationShutdownReason shutdownReason = System.Web.Hosting.HostingEnvironment.ShutdownReason;
            Logger.Information("App is shutting down (reason = {@shutdownReason})", shutdownReason);

            // WARNING : Some code runs AFTER Application_End ... see AppPostShutDown
        }
    }

然后 App_Start 文件夹下的几个 类(其中一些你可以忽略 :P ):

  • AppPreStart.cs
using XXX;
using Serilog;


[assembly: WebActivatorEx.PreApplicationStartMethod(typeof(AppPreStart), nameof(AppPreStart.PreApplicationStart))]
namespace XXX
{
    /// <summary>
    /// This runs even before global.asax Application_Start (see WebActivatorConfig)
    /// </summary>
    public class AppPreStart
    {
        public static void PreApplicationStart()
        {
            LogConfig.Configure();
            var logger = Log.ForContext<AppPreStart>();
            logger.Information("App is starting ...");

            // ... snip ...
            // very early things like IoC config, AutoMapper config ...
            // ... snip ...

            logger.Debug("Done with AppPreStart");
        }
    }
}
  • AppPostShutDown.cs
using XXX;
using Serilog;

[assembly: WebActivatorEx.ApplicationShutdownMethod(typeof(AppPostShutDown), nameof(AppPostShutDown.PostApplicationShutDown))]
namespace XXX
{
    /// <summary>
    /// This runs even before global.asax Application_Start (see WebActivatorConfig)
    /// </summary>
    public class AppPostShutDown
    {
        private static ILogger Logger = Log.ForContext<AppPostShutDown>();

        public static void PostApplicationShutDown()
        {
            Logger.Debug("PostApplicationShutDown");

            // ... snip ...
            // very late things like IoC dispose ....
            // ... snip ...

            // force flushing the last "not logged" events
            Logger.Debug("Closing the logger! ");
            Log.CloseAndFlush();
        }
    }
}
  • LogConfig.cs
using Serilog;
using Serilog.Events;
using SerilogWeb.Classic;
using SerilogWeb.Classic.Enrichers;
using SerilogWeb.Classic.WebApi.Enrichers;

namespace XXX
{
    public class LogConfig
    {
        static public void Configure()
        {
            ApplicationLifecycleModule.LogPostedFormData = LogPostedFormDataOption.OnlyOnError;
            ApplicationLifecycleModule.FormDataLoggingLevel = LogEventLevel.Debug;
            ApplicationLifecycleModule.RequestLoggingLevel = LogEventLevel.Debug;

            var loggerConfiguration = new LoggerConfiguration().ReadFrom.AppSettings()
                    .Enrich.FromLogContext()
                    .Enrich.With<HttpRequestIdEnricher>()
                    .Enrich.With<UserNameEnricher>()
                    .Enrich.With<HttpRequestUrlEnricher>()
                    .Enrich.With<WebApiRouteTemplateEnricher>()
                    .Enrich.With<WebApiControllerNameEnricher>()
                    .Enrich.With<WebApiRouteDataEnricher>()
                    .Enrich.With<WebApiActionNameEnricher>()
                ;

            Log.Logger = loggerConfiguration.CreateLogger();
        }
    }
}

然后从 Web.config 中读取日志配置的可变部分,在 AppSettings 中具有以下键:

    <!-- SeriLog-->
    <add key="serilog:level-switch:$controlSwitch" value="Information" />
    <add key="serilog:minimum-level:controlled-by" value="$controlSwitch" />
    <add key="serilog:enrich:with-property:AppName" value="XXXApp" />
    <add key="serilog:enrich:with-property:AppComponent" value="XXXComponent" />
    <add key="serilog:enrich:with-property:Environment" value="Dev" />
    <add key="serilog:enrich:with-property:MachineName" value="%COMPUTERNAME%" />
    <add key="serilog:using:Seq" value="Serilog.Sinks.Seq" />
    <add key="serilog:write-to:Seq.serverUrl" value="http://localhost:5341" />
    <add key="serilog:write-to:Seq.apiKey" value="xxxxxxxxxxx" />
    <add key="serilog:write-to:Seq.controlLevelSwitch" value="$controlSwitch" />

(然后我们进行了 Web.config 转换,将其转换为 "tokenized" 生产文件

Web.Release.config

    <!-- SeriLog-->
    <add key="serilog:enrich:with-property:Environment" value="__Release_EnvironmentName__"
         xdt:Transform="Replace" xdt:Locator="Match(key)"/>

    <add key="serilog:write-to:Seq.serverUrl" value="__app_serilogSeqUrl__"
         xdt:Transform="Replace" xdt:Locator="Match(key)"/>
    <add key="serilog:write-to:Seq.apiKey" value="__app_serilogApiKey__"
         xdt:Transform="Replace" xdt:Locator="Match(key)"/>

此配置的一些最重要的部分是:

  • 尽快配置您的记录器
  • 在最后调用 Log.CloseAndFlush(); 以确保您的所有日志事件都是 stored/pushed
  • 添加来自 Serilog 的 Enrich.FromLogContext(),以及来自 SerilogWeb.ClassicSerilogWeb.WebApi 的一些 enrichers,它们会变得非常有用。
  • 记录到正确支持结构化日志记录的日志服务器(写入文件有太多缺点)......我们使用 Seq 并且对此非常满意(在每台开发机器上本地安装,然后是生产中的集中实例)。它支持 searching/querying 和仪表板以及动态日志级别控制。