1. 程式人生 > >玩轉ASP.NET Core中的日誌組件

玩轉ASP.NET Core中的日誌組件

文件夾 tst allow sting fault optional manage erp www

簡介

日誌組件,作為程序員使用頻率最高的組件,給程序員開發調試程序提供了必要的信息。ASP.NET Core中內置了一個通用日誌接口ILogger,並實現了多種內置的日誌提供器,例如

  • Console
  • Debug
  • EventSource
  • EventLog
  • TraceSource
  • Azure App Service

除了內置的日誌提供器,ASP.NET Core還支持了多種第三方日誌工具,例如

  • elmah.io
  • Gelf
  • JSNLog
  • KissLog.net
  • Loggr
  • NLog
  • Serilog

開發人員在ASP.Net Core中可以自由指定日誌提供器,並將日誌發送到指定的位置。
本篇博文中,我們將由淺入深的介紹ASP.Net Core中通用日誌接口,最後我們將實現一些自定義的日誌提供器(Log Provider)。

使用系統提供的內置日誌提供器

日誌級別(Log Level)

ASP.NET Core中提供了6種日誌級別,分別是Trace, Debug, Information, Warning, Error, Critical。以下是他們的具體使用場景

日誌級別 常用場景
Trace 記錄一些對程序員調試問題有幫助的信息,
其中可能包含一些敏感信息, 所以應該避免在
生產環境中啟用Trace日誌
Debug 記錄一些在開發和調試階段有用的短時變
量(Short-term usefulness), 所以除非為了臨時排除生產環境的
故障,開發人員應該盡量避免在生產環境中啟用Debug日誌
Information 記錄應用程序的一些流程, 例如,記錄當前api請求的url
Warning 記錄應用程序中發生的不正常或者未預期的事件信息。
這些信息中可能包含錯誤消息或者錯誤產生的條件, 例如, 文件未找到
Error 記錄應用程序中某個操作產生的錯誤和異常信息。
Critical 記錄一些需要立刻修復的問題。例如數據丟失,磁盤空間不足。

如何創建日誌

為了創建一個日誌,我們首先需要通過依賴註入獲取一個實現ILogger<日誌類別>的泛型接口對象。

已以下代碼為例, 在ValuesController的構造函數中,我們註入了一個ILogger的日誌記錄器

    [Route("api/[controller]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        private readonly ILogger<ValuesController> _logger = null;

        public ValuesController(ILogger<ValuesController> logger)
        {
            _logger = logger;
        }

        // GET api/values
        [HttpGet]
        public ActionResult<IEnumerable<string>> Get()
        {
            _logger.LogInformation("[Test Log]Getting items.");
            return new string[] { "value1", "value2" };
        }


    }

然後我們使用ILogger接口提供的LogInformation方法添加了一個Information類型日誌"[Test Log]Getting items"。

註:ILogger為了提供了6個可用的輸出日誌方法,分別對應了6個不同的日誌級別

  • LogTrace
  • LogDebug
  • LogInformation
  • LogWarning
  • LogError
  • LogCritical!

下面我們使用Kestral服務器啟動項目
技術分享圖片

項目產生的日誌如下,我們手動輸出的日誌出現在控制臺中。

技術分享圖片

日誌配置

可能針對以上的代碼,有些同學可能有疑問,我們沒有在Startup.cs中註入任何日誌提供器,但是日誌卻正常產生了。這是由於Program.cs中默認使用WebHost.CreateDefaultBuilder方法添加了2個日誌提供器。

默認日誌提供器

當創建一個新的ASP.NET Core WebApi項目,我們通常在Program.cs中看到以下代碼。

    public class Program
    {
        public static void Main(string[] args)
        {
            CreateWebHostBuilder(args).Build().Run();
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>();
    }

下面我們看一下WebHost.CreateDefaultBuilder的源代碼

    public static IWebHostBuilder CreateDefaultBuilder(string[] args)
    {
        var builder = new WebHostBuilder()
            .UseKestrel()
            .UseContentRoot(Directory.GetCurrentDirectory())
            .ConfigureAppConfiguration((hostingContext, config) =>
            {
                var env = hostingContext.HostingEnvironment;

                config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                      .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);

                if (env.IsDevelopment())
                {
                    var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
                    if (appAssembly != null)
                    {
                        config.AddUserSecrets(appAssembly, optional: true);
                    }
                }

                config.AddEnvironmentVariables();

                if (args != null)
                {
                    config.AddCommandLine(args);
                }
            })
            .ConfigureLogging((hostingContext, logging) =>
            {
                logging.UseConfiguration(hostingContext.Configuration.GetSection("Logging"));
                logging.AddConsole();
                logging.AddDebug();
            })
            .UseIISIntegration()
            .UseDefaultServiceProvider((context, options) =>
            {
                options.ValidateScopes = context.HostingEnvironment.IsDevelopment();
            })
            .ConfigureServices(services =>
            {
                services.AddTransient<IConfigureOptions<KestrelServerOptions>, KestrelServerOptionsSetup>();
            });

        return builder;
    }

你會發現代碼中通過logging.AddConsolelogging.AddDebug默認配置了Console和Debug類型的日誌提供器,這也就是為什麽我們沒有註入任何日誌提供器,日誌卻能正常產生了。

手動添加日誌提供器

看了以上代碼後,你應該可以也清楚了如何自己添加其他內置的日誌提供器。我們只需要在Program.cs中使用ConfigureLogging方法就可以配置我們需要的日誌提供器了。

例:

    public class Program
    {
        public static void Main(string[] args)
        {
            CreateWebHostBuilder(args).Build().Run();
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
            .ConfigureLogging((hostingContext, logging) =>
            {
                logging.AddConsole();
                logging.AddDebug();
                logging.AddEventSourceLogger();
            })
            .UseStartup<Startup>();
    }

除了在Program.cs添加日誌提供器之外,我們還可以在Startup.cs中添加日誌提供器。
在Startup.cs中,我們可以為Configure方法添加第三個參數ILoggerFactory loggerFactory, 並使用該參數添加日誌提供器。

例:

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        loggerFactory.AddConsole();
        loggerFactory.AddDebug();
        
        app.UseMvc();
    }

配置文件及日誌級別過濾

ASP.NET Core默認會從appSetting.json中的Logging屬性讀取日誌的配置(當然你也可以從其他文件中讀取配置),這裏設置了不同的日誌提供器產生的最低的日誌級別,配置樣例如下。

{
  "Logging": {
    "Debug": {
      "LogLevel": {
        "Default": "Information"
      }
    },
    "Console": {
      "LogLevel": {
        "Microsoft.AspNetCore.Mvc.Razor.Internal": "Warning",
        "Microsoft.AspNetCore.Mvc.Razor.Razor": "Debug",
        "Microsoft.AspNetCore.Mvc.Razor": "Error",
        "Default": "Trace"
      }
    },
    "LogLevel": {
      "Default": "Debug"
    }
  }
}

以上代碼中的Debug表示Debug日誌提供器, Console表示Console日誌提供器, 最後一個LogLevel表示其他日誌提供器通用。
Debug中的Default設置為Information, 即Debug中產生的日誌最低級別是Information, 低於Information級別的日誌不會輸出。Console中的配置同理。

自定義日誌組件

在學習了以上基礎知識之後,我們應該對內置的日誌提供器有了簡單的認識。下面我們嘗試自定義2個日誌提供器。
在ASP.NET Core中,我們可以通過實現ILogger, ILoggerProvider2個接口來創建我們自己的日誌提供器。

編寫一個自定義樣式的控制臺日誌組件

這裏我們希望添加一個在控制臺中輸出的日誌,但是和內置Console類型日誌的區別是我們為不同的日誌類型設置了不同的顏色。

首先我們創建一個新的Api項目ColoredConsoleLoggerSample
技術分享圖片

然後我們創建一個針對不同日誌級別的字體顏色配置類ColoredConsoleLoggerConfiguration, 代碼如下

    public class ColoredConsoleLoggerConfiguration
    {
        public LogLevel LogLevel { get; set; } = LogLevel.Warning;
        public ConsoleColor Color { get; set; } = ConsoleColor.Yellow;
    }

這個類中定義了針對不同日誌類型設置不同的字體顏色。

然後我們創建一個日誌類ColoredConsoleLogger, 它實現了ILogger接口,代碼如下

    public class ColoredConsoleLogger : ILogger
    {
        private readonly string _name;
        private readonly ColoredConsoleLoggerConfiguration _config;

        public ColoredConsoleLogger(string name, ColoredConsoleLoggerConfiguration config)
        {
            _name = name;
            _config = config;
        }

        public IDisposable BeginScope<TState>(TState state)
        {
            return null;
        }

        public bool IsEnabled(LogLevel logLevel)
        {
            return logLevel == _config.LogLevel;
        }

        public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
        {
            if (!IsEnabled(logLevel))
            {
                return;
            }
            
            var color = Console.ForegroundColor;
            Console.ForegroundColor = _config.Color;
            Console.WriteLine($"{logLevel.ToString()} - {_name} - {formatter(state, exception)}");
            Console.ForegroundColor = color;
        }
    }

代碼解釋

  • ColoredConsoleLogger僅針對一種日誌級別
  • 只要當前產生的日誌級別和ColoredConsoleLogger中定義的日誌級別一樣時,日誌才會輸出,這裏我們是用IsEnable方法判斷的
  • Log是ILogger接口中定義的方法,我們就是在這個方法中輸出日誌的
  • 這裏我們在輸入日誌前記錄下了當前控制臺的原始字體顏色, 當輸出日誌完成之後,我們將字體顏色恢復成了原來的顏色

然後我們添加一個Logger提供器類ColoredConsoleLoggerProvider,代碼如下

    public class ColoredConsoleLoggerProvider : ILoggerProvider
    {
        private readonly ColoredConsoleLoggerConfiguration _config;

        public ColoredConsoleLoggerProvider(ColoredConsoleLoggerConfiguration config)
        {
            _config = config;
        }

        public ILogger CreateLogger(string categoryName)
        {
            return new ColoredConsoleLogger(categoryName, _config);
        }

        public void Dispose()
        {

        }
    }

代碼解釋

  • ColoredConsoleLoggerProvider僅針對一種日誌級別
  • CreateLoggerILoggerProvider接口中定義的方法,它是用來返回一個日誌生成器的

最後我們修改Startup.cs中的Configure方法, 代碼如下

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        
        loggerFactory.AddProvider(new ColoredConsoleLoggerProvider(new ColoredConsoleLoggerConfiguration
        {
            LogLevel = LogLevel.Information,
            Color = ConsoleColor.Blue
        }));
        loggerFactory.AddProvider(new ColoredConsoleLoggerProvider(new ColoredConsoleLoggerConfiguration
        {
            LogLevel = LogLevel.Debug,
            Color = ConsoleColor.Gray
        }));

        app.UseMvc();
    }

這裏我們添加了2個日誌日誌提供器,分別是針對Information級別日誌和Debug級別日誌的

最終效果

技術分享圖片

我們的日誌根據我們預設的字體顏色正確的顯示了出來

編寫一個與SignalR集成的實時日誌組件

下面我們再來自定義一個與SignalR集成的日誌提供器,我們希望產生的日誌通過一個SignalR服務器推送到一個網頁中。

首先我們創建一個ASP.NET Core WebApi項目,命名為SignalrServer, 創建之後,我們右鍵項目屬性,修改App Url為http://localhost:5000
技術分享圖片

然後我們創建一個LogHub類,它集成自Hub類,代碼如下

    public class LogHub : Hub
    {
        public async Task WriteLog(Log log)
        {
            await Clients.All.SendAsync("showLog", log);
        }
    }

    public class Log
    {
        public LogLevel Level { get; set; }

        public string Content { get; set; }
    }

代碼解釋

  • 這裏我們創建了一個寫日誌的方法,它會把日誌推送到所有連接到SignalR服務器的客戶端,並調用客戶端的showLog方法來展示推送的日誌信息。

然後我們修改Startup.cs文件,代碼如下

    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddCors();
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
            
            services.AddSignalR();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseCors(p => p.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader().AllowCredentials());
            app.UseSignalR(routes =>
            {
                routes.MapHub<LogHub>("/logHub");
            });

            

            app.UseMvc();
        }
    }

代碼解釋

  • 我們通過service.AddSignalR註冊了SignalR服務
  • 我們通過app.UserSignalR方法註冊一個logHub
  • 這裏我們啟用了CORS, 因為需要提供跨域訪問

然後我們創建一個另外一個ASP.NET Core WebApi項目, SignalRLoggerSample
技術分享圖片

項目創建成功之後,我們右鍵點擊項目屬性,並設置App URL為http://localhost:5001

技術分享圖片

然後我們使用Package Console Manager, 安裝Microsoft.AspNetCore.SignalR.Client

PM> install-package Microsoft.AspNetCore.SignalR.Client

為了創建一個SignalR日誌提供器, 我們分別創建一個SignalRLogger類和一個SignalRLoggerProvider類, 代碼如下

SignalRLogger.cs

    public class SignalRLogger : ILogger
    {
        HubConnection connection;

        public SignalRLogger()
        {
            connection = new HubConnectionBuilder()
             .WithUrl("http://localhost:5000/LogHub")
             .Build();
        }

        public IDisposable BeginScope<TState>(TState state)
        {
            return null;
        }

        public bool IsEnabled(LogLevel logLevel)
        {
            return true;
        }

        public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
        {
            if (!IsEnabled(logLevel))
            {
                return;
            }

            connection.StartAsync().Wait();
            var task = connection.SendAsync("writeLog", new { Level = logLevel, Content = formatter(state, exception) });
            task.Wait();
        }
    }

SignalRLoggerProvider.cs

    public class SignalRLoggerProvider : ILoggerProvider
    {
        public SignalRLoggerProvider()
        {
        }

        public ILogger CreateLogger(string categoryName)
        {
            return new SignalRLogger();
        }

        public void Dispose()
        {

        }
    }

代碼解釋

  • 這裏使用HubConnectionBuilder創建了一個SignalR連接
  • 連接啟動成功之後,我們使用connection.SendAsync方法,將當前產生的日誌信息發送到SignalR服務器

添加完成之後,我們在wwwroot文件夾中創建一個index.html, 在其中引入jquery和signalr的js庫,並指定連接的signalR服務器是http://localhost:5000/logHub

index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title></title>
    <script src="jquery-1.10.2.min.js"></script>
    <script src="signalr.min.js"></script>
</head>
<body>
    <h1>Logs</h1>
    <div id="content" style="border:1px solid #0094ff">

    </div>
    <script type="text/javascript">
        var levels = [
            { level: 0, name: 'Trace', backgroundColor: 'gray' },
            { level: 1, name: 'Debug', backgroundColor: 'green' },
            { level: 2, name: 'Information', backgroundColor: 'blue' },
            { level: 3, name: 'Warning', backgroundColor: 'yellow' },
            { level: 4, name: 'Error', backgroundColor: 'orange' },
            { level: 5, name: 'Critical', backgroundColor: 'red' },
        ];

        function getLevelName(level) {
            return levels.find(function (o) {
                return o.level == level;
            }).name;
        }

        function getLevelColor(level) {
            return levels.find(function (o) {
                return o.level == level;
            }).backgroundColor;
        }

        var connection = new signalR.HubConnectionBuilder().withUrl("http://localhost:5000/logHub").build();

        connection.on("showLog", function (message) {
            var div = "<div style='background-color:" + getLevelColor(message.level)+"'>[" + getLevelName(message.level) + "]:" + message.content + "</div>";
            $("#content").append(div);
        });

        connection.start().catch(function (err) {
            return console.error(err.toString());
        });
    </script>
</body>
</html>

然後我們修改ValuesController文件,代碼如下

    [Route("api/[controller]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        private ILogger<ValuesController> _logger = null;

        public ValuesController(ILogger<ValuesController> logger)
        {
            _logger = logger;
        }

        // GET api/values
        [HttpGet]
        public ActionResult<IEnumerable<string>> Get()
        {
            _logger.LogTrace("User call the /api/values api");
            _logger.LogDebug("User call the /api/values api");
            _logger.LogInformation("User call the /api/values api");
            _logger.LogWarning("User call the /api/values api");
            _logger.LogError("User call the /api/values api");
            _logger.LogCritical("User call the /api/values api");
            return new string[] { "value1", "value2" };
        }
    }

代碼解釋

  • 我們創建了一個ValueController類的日誌
  • 當用戶請求/api/values時,我們輸出了6個不同級別的日誌

最後我們修改Startup.cs中的Configure方法,使用我們之前介紹的方法,將SignalRLoggerProvider添加到管道中

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        app.UseStaticFiles();

        loggerFactory.AddProvider(new SignalRLoggerProvider());

        app.UseMvc();


    }

最終效果

最後我們按照順序,先啟動SignalRServer, 再啟動SignalRLoggerSample, 效果如下
技術分享圖片

本篇源代碼

參考文獻

  • MSDN: Logging in ASP.NET Core
  • How to add custom logging in ASP.NET Core?

玩轉ASP.NET Core中的日誌組件