Elasticsearch, Kibana 和 Docker 使用 .NET Standard 2





5.00/5 (28投票s)
现代 .NET 应用程序中的高级日志记录。一个周日上午的POC(概念验证)。
引言
在本文中,您将学习如何设置开发环境,以便出于日志记录目的使用 Elasticsearch 和 Kibana。在此过程中,您将使用 Docker,非常基础的用法,并且您还将了解到在我们的 Windows 经典桌面应用程序中使用 .NET Standard 库是多么容易。
背景
我将演示在我们的系统中集成 Elasticsearch 和 Kibana 是非常容易的。为此,我将设置并配置一个 Docker 网络和两个 Docker 镜像,一个用于 Elasticsearch,另一个用于 Kibana。这个基础设施将由一个 Windows 经典桌面控制台应用程序使用,该应用程序将执行一些随机日志记录。继续我上一篇文章中关于.NET Core 中的正确日志记录的内容,我将配置三个日志输出:控制台,以及使用 Serilog,我将配置并使用一个滚动文件接收器和一个 Elasticsearch 接收器。这些接收器将用于 .NET Standard 2 库中,但该库将被控制台应用程序引用,并使用最新的 Microsoft Dependency Injection Extensions。
设置基础设施
首先,我们需要在我们的机器上安装并配置好 Docker,我使用的是 W10 和 Docker for Windows。(如果您还没有,可以在这里找到)。我们只需要运行三个命令就可以用 Docker 完成所有设置。(这很棒,不是吗?)
docker network create elk-logging-poc --driver=bridge
docker run -p 5601:5601 --name kibana -d --network elk-logging-poc kibana
docker run -p 9200:9200 -p 9300:9300 --name elasticsearch -d --network elk-logging-poc elasticsearch
第一个命令创建一个用于此 POC 的网络,第二个命令运行 Kibana,如果本地不存在则从 Docker Hub 拉取镜像,第三个命令对 Elasticsearch 执行相同的操作。
初始状态
- 创建网络
- 安装并运行 Kibana
此时,Kibana 已安装,但如果我们访问 localhost:5601,可以看到 Kibana 缺少 Elasticsearch。
安装 Elasticsearch
现在 Kibana 已准备好进行配置。
设置解决方案
好的,现在是时候设置我们的解决方案了,正如我所说,我将使用一个 Windows 经典桌面控制台应用程序和一个 .NET Standard 库。所以,我将创建一个新的解决方案和 WCD 控制台项目。
提示:虽然这是一个 POC,但我始终尝试使用健壮的命名空间,我始终牢记 POC 可能会变成原型,然后变成产品。这些事情确实会发生。
现在,让我们创建 .NET Standard 库。
结果将是这样的:
控制台文件
为了节省您的时间,您需要将 packages.config 中列出的 NuGet 包手动安装到控制台应用程序中,手动操作更方便。
packages.config
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Elasticsearch.Net"
version="5.5.0" targetFramework="net462" />
<package id="Microsoft.DotNet.InternalAbstractions"
version="1.0.0" targetFramework="net462" />
<package id="Microsoft.Extensions.Configuration"
version="2.0.0" targetFramework="net462" />
<package id="Microsoft.Extensions.Configuration.Abstractions"
version="2.0.0" targetFramework="net462" />
<package id="Microsoft.Extensions.Configuration.Binder"
version="2.0.0" targetFramework="net462" />
<package id="Microsoft.Extensions.Configuration.FileExtensions"
version="2.0.0" targetFramework="net462" />
<package id="Microsoft.Extensions.Configuration.Json"
version="2.0.0" targetFramework="net462" />
<package id="Microsoft.Extensions.DependencyInjection"
version="2.0.0" targetFramework="net462" />
<package id="Microsoft.Extensions.DependencyInjection.Abstractions"
version="2.0.0" targetFramework="net462" />
<package id="Microsoft.Extensions.DependencyModel"
version="1.0.0" targetFramework="net462" />
<package id="Microsoft.Extensions.FileProviders.Abstractions"
version="2.0.0" targetFramework="net462" />
<package id="Microsoft.Extensions.FileProviders.Physical"
version="2.0.0" targetFramework="net462" />
<package id="Microsoft.Extensions.FileSystemGlobbing"
version="2.0.0" targetFramework="net462" />
<package id="Microsoft.Extensions.Logging"
version="2.0.0" targetFramework="net462" />
<package id="Microsoft.Extensions.Logging.Abstractions"
version="2.0.0" targetFramework="net462" />
<package id="Microsoft.Extensions.Logging.Console"
version="2.0.0" targetFramework="net462" />
<package id="Microsoft.Extensions.Logging.Debug"
version="2.0.0" targetFramework="net462" />
<package id="Microsoft.Extensions.Options"
version="2.0.0" targetFramework="net462" />
<package id="Microsoft.Extensions.Options.ConfigurationExtensions"
version="2.0.0" targetFramework="net462" />
<package id="Microsoft.Extensions.Primitives"
version="2.0.0" targetFramework="net462" />
<package id="Newtonsoft.Json"
version="10.0.1" targetFramework="net462" />
<package id="Serilog"
version="2.5.0" targetFramework="net462" />
<package id="Serilog.Extensions.Logging"
version="2.0.2" targetFramework="net462" />
<package id="Serilog.Settings.Configuration"
version="2.4.0" targetFramework="net462" />
<package id="Serilog.Sinks.Elasticsearch"
version="5.4.0" targetFramework="net462" />
<package id="Serilog.Sinks.File"
version="3.2.0" targetFramework="net462" />
<package id="Serilog.Sinks.PeriodicBatching"
version="2.1.0" targetFramework="net462" />
<package id="Serilog.Sinks.RollingFile"
version="3.3.0" targetFramework="net462" />
<package id="System.Linq"
version="4.1.0" targetFramework="net462" />
<package id="System.Resources.ResourceManager"
version="4.0.1" targetFramework="net462" />
<package id="System.Runtime"
version="4.1.0" targetFramework="net462" />
<package id="System.Runtime.CompilerServices.Unsafe"
version="4.4.0" targetFramework="net462" />
</packages
appsettings.json
{
"Logging": {
"IncludeScopes": true,
"Debug": {
"LogLevel": {
"Default": "Critical"
}
},
"Console": {
"LogLevel": {
"Microsoft.AspNetCore.Mvc.Razor.Internal": "Warning",
"Microsoft.AspNetCore.Mvc.Razor.Razor": "Debug",
"Microsoft.AspNetCore.Mvc.Razor": "Error",
"Default": "Critical"
}
},
"LogLevel": {
"Default": "Critical"
}
},
"Serilog": {
"WriteTo": [
{
"Name": "Elasticsearch",
"Args": {
"nodeUris": "https://:9200;http://remotehost:9200/",
"indexFormat": "elk-poc-index-{0:yyyy.MM}",
"templateName": "myCustomTemplate",
"typeName": "myCustomLogEventType",
"pipelineName": "myCustomPipelineName",
"batchPostingLimit": 50,
"period": 2000,
"inlineFields": true,
"minimumLogEventLevel": "Trace",
"bufferBaseFilename": "C:/Logs/docker-elk-serilog-web-buffer",
"bufferFileSizeLimitBytes": 5242880,
"bufferLogShippingInterval": 5000,
"connectionGlobalHeaders":
"Authorization=Bearer SOME-TOKEN;OtherHeader=OTHER-HEADER-VALUE"
}
}
],
"LogFile": "C:/Logs/ElasticSearchPoc.log",
"MinimumLevel": "Information"
}
}
Program.cs
using ElasticSearchPoc.Domain.LogProducer;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog;
using System;
namespace ElasticSearchPoc.Presentation.WCD.Console
{
class Program
{
static void Main(string[] args)
{
// Create service collection
var serviceCollection = new ServiceCollection();
ConfigureServices(serviceCollection);
// Create service provider
var serviceProvider = serviceCollection.BuildServiceProvider();
// Run app (Every execution should create a new RunId)
serviceProvider.GetService<BasicLogProducer>().Run();
}
private static void ConfigureServices(IServiceCollection serviceCollection)
{
// Build configuration
var configuration = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("appsettings.json", false)
.Build();
// Add console logging
serviceCollection.AddSingleton(new LoggerFactory()
.AddConsole(configuration.GetSection("Logging"))
.AddSerilog()
.AddDebug());
serviceCollection.AddLogging();
// Add Serilog logging
Log.Logger = new LoggerConfiguration()
.WriteTo.RollingFile(configuration["Serilog:LogFile"])
.ReadFrom.Configuration(configuration)
.CreateLogger();
// Add access to generic IConfigurationRoot
serviceCollection.AddSingleton(configuration);
// Add the App
serviceCollection.AddTransient<BasicLogProducer>();
}
}
}
我想对这些文件添加一些评论,请注意 appsettings.json 中的不同日志级别以及 Elasticsearch 选项,设置是此 POC 的关键。此外,我想强调的是,我们的程序源代码是干净的并且遵循 SOLID 原则,我必须公开感谢 .NET Foundation 及其贡献者提供的这些极其有用的库和扩展。
让我们看一下 .NET Standard 库中的文件,LogProducer
。
LogProducer 文件
正如您所见,它唯一的依赖是 Microsoft.Extensions.Logging
...
BasicLogProducer.cs
using Microsoft.Extensions.Logging;
using System;
using System.Threading;
namespace ElasticSearchPoc.Domain.LogProducer
{
public class BasicLogProducer
{
private readonly ILogger<BasicLogProducer> _logger;
public BasicLogProducer(ILogger<BasicLogProducer> logger)
{
_logger = logger;
}
public void Run()
{
var runDate = DateTime.Now;
while (true)
{
// Let's randomize our logs...
Array values = Enum.GetValues(typeof(LogLevel));
Random random = new Random();
LogLevel randomLogLevel = (LogLevel)values.GetValue(random.Next(values.Length));
switch (randomLogLevel)
{
case LogLevel.Trace:
_logger.LogTrace($"RunDate: {runDate};
Message Id: {Guid.NewGuid()}; LogLevel: Trace;
LogLevelValue: {randomLogLevel.ToString("D")}");
break;
case LogLevel.Debug:
_logger.LogDebug($"RunDate: {runDate};
Message Id: {Guid.NewGuid()}; LogLevel: Debug;
LogLevelValue: {randomLogLevel.ToString("D")}");
break;
case LogLevel.Information:
_logger.LogInformation($"RunDate: {runDate};
Message Id: {Guid.NewGuid()}; LogLevel: Information;
LogLevelValue: {randomLogLevel.ToString("D")}");
break;
case LogLevel.Warning:
_logger.LogWarning($"RunDate: {runDate};
Message Id: {Guid.NewGuid()}; LogLevel: Warning;
LogLevelValue: {randomLogLevel.ToString("D")}");
break;
case LogLevel.Error:
_logger.LogError($"RunDate: {runDate};
Message Id: {Guid.NewGuid()}; LogLevel: Error;
LogLevelValue: {randomLogLevel.ToString("D")}");
break;
case LogLevel.Critical:
_logger.LogCritical($"RunDate: {runDate};
Message Id: {Guid.NewGuid()}; LogLevel: Critical;
LogLevelValue: {randomLogLevel.ToString("D")}");
break;
case LogLevel.None:
default:
break;
}
Thread.Sleep(100);
}
}
}
}
虽然我总是尽量编写自解释的源代码,但这个类值得简要解释。它使用了 DI(依赖注入),ILogger<T>
是被创建的,因为宿主应用程序注册了 ILoggingFactory
和 Serilog
,所以该日志记录器将拥有我们在主应用程序中配置的所有内容,在本例中是我们的三个接收器:控制台、文件和 Elasticsearch。Run
方法获取初始运行日期并开始生成具有随机日志级别的日志。它非常基础,但能完成它的工作。
配置、运行和测试
我不会提供太多细节。我猜通过一些截图,您会喜欢自己尝试一下。我们首先要做的是在 Kibana 中配置主索引模式,正如您所见,这是我们 appsettings.json 文件中的一个配置参数。我使用了 "indexFormat": "elk-poc-index-{0:yyyy.MM}"
,所以我们需要将 Kibana 索引配置为 "elk-poc-index-*
"。
好了,让我们启动应用程序看看会发生什么……
控制台只记录 Critical 级别,根据配置参数。
我们还记录到文件,如配置文件和我们的 Main
方法中所述,我将通过几张截图演示如何使用我所知的最佳 Tail 工具,Tail Blazer(如果您一直读到这里并且不知道 TailBlazer
,那么您现在就必须去尝试一下,因为您会爱上它)。
- 纯文本输出
- 点击右上角的齿轮图标,我们可以添加一些高亮。
- 在主窗口中,我们也可以添加一些过滤器。
我将输入 Fatal,只查看包含 Fatal
关键字的条目。
那么,Elasticsearch 和 Kibana 呢? 好了,在我截取这些截图的时候,控制台应用程序一直在生成日志,在控制台(仅 Critical),在本地文件系统中的文件里(如我们通过 TailBlazer 看到的 Information 级别),并且程序一直在向 Elasticsearch 接收器发送日志(Trace 级别),所以,如果我们进入 Kibana 并选择左侧菜单中的 Timelion 菜单项,我们应该能看到一个显示接收到的日志数量的图表,类似于这样。
图表有意义,因为代码每秒发送大约 10 条日志,因为有一个 Thread.Sleep(100)
,对吧?让我们稍微加速应用程序,将延迟设置为仅 10 毫秒。如果我再次运行它,时间线看起来是这样的。
如果我取消延迟并强迫我的机器生成尽可能多的日志会怎样?
嗯,文件日志增长得非常快,正如预期的那样(蓝色表示最近创建,请查看时间戳中的毫秒)。
CPU 达到 100%(也符合预期)。
Kibana 每秒接收到 772 条日志的高峰。考虑到所有东西都在同一台机器上运行,而且我正在调试和监视日志文件,这不算太糟,我们可能还可以进一步提升。
好了,今天就到这里吧,我仍然不知道如何正确地可视化 Kibana 中的数据,但 POC 在此结束,因为它正如预期那样记录了。
关注点
- .NET 标准库是可能的最佳解决方案,因为它们是当今最可重用的选择。
- 使用 Docker 配置 Elasticsearch 和 Kibana 只需三个步骤。
- 现代 .NET 应用程序的日志记录有许多可能性,我们已经学会了如何
- 正确地记录到控制台
- 正确地记录到日志文件以及如何使用 TailBlazer 实时可视化它们
- 如何配置 Serilog 接收器以记录到 Elasticsearch
历史
在家里的一个周日 POC,我可能不会再添加任何东西,除非进行一些拼写更正或根据要求进行一些澄清。