扩展Serilog实现日志推送平台

最近在完成一个服务管理平台,提供可视化管理车间控制服务,包括服务的启停,日志的查看。在各服务中使用 Serilog 对日志进行记录,推送到服务管理平台,再进行统一分发,各客户端同步查看服务运行日志。

Serilog

Serilog 是一个用于 .NET 应用程序的诊断日志库。它易于设置,具有干净的 API,并且可以在所有最新的 .NET 平台上运行。虽然即使在最简单的应用程序中也很有用,但 Serilog 对结构化日志记录的支持在检测复杂、分布式和异步应用程序和系统时大放异彩,具体可以查看Serilog-Github

Serilog 提供了非常简单的扩展API,只需要继承 ILogEventSink 并实现 Emit 方法即可,下文提供了两个 Web 扩展。

HTTP扩展

编写 HttpSink 继承 ILogEventSink ,使用POST进行数据发送。

public class HttpSink : ILogEventSink
{
    private readonly Uri _uri;
    private readonly IFormatProvider _formatProvider;

    public HttpSink(Uri uri, IFormatProvider formatProvider)
    {
        _uri = uri;
        _formatProvider = formatProvider;
    }

    public async void Emit(LogEvent logEvent)
    {
        var message = logEvent.RenderMessage(_formatProvider);
        var req = new LogMessage(logEvent.Level.ToString(), message);
        byte[] b = JsonSerializer.SerializeToUtf8Bytes(req);
        var s = Encoding.UTF8.GetString(b);
        await new HttpClient().PostAsync(_uri, new StringContent(s, Encoding.UTF8, "application/json"));
    }
}

编写扩展方法,提供配置入口。可以根据实际需求重载很多方法,例如传入 string 类型的 url,或者提供用于通过服务验证的凭证等。

public static class LoggerConfigurationHttpRExtensions
{

    public static LoggerConfiguration Http(
        this LoggerSinkConfiguration loggerConfiguration,
        Uri uri,
        IFormatProvider formatProvider = null)
    {
        if (loggerConfiguration == null) 
             throw new ArgumentNullException(nameof(loggerConfiguration));

        if (uri == null) 
             throw new ArgumentNullException(nameof(uri));

        return loggerConfiguration.Sink(new HttpSink(uri, formatProvider));
    }
}

SignalR扩展

同样的,编写 SignalRSink 继承于 ILogEventSink,通过内置的 Invoke 实现消息的发送。

public class SignalRSink : ILogEventSink
{
    private readonly Uri _uri;
    private readonly string _channel;
    private readonly IFormatProvider _formatProvider;
    private readonly HubConnection _hub;

    public SignalRSink(Uri uri, string channel, IFormatProvider formatProvider)
    {
        _uri = uri;
        _channel = channel;
        _formatProvider = formatProvider;
        _hub = new HubConnectionBuilder()
            .WithUrl(uri)
            .Build();
    }

    public async void Emit(LogEvent logEvent)
    {
        if (_hub.State == HubConnectionState.Disconnected)
        {
            await _hub.StartAsync();
        }

        var message = logEvent.RenderMessage(_formatProvider);

        var req = new LogMessage(logEvent.Level.ToString(), message);
        await _hub.InvokeAsync(_channel, req);
    }
}

本文实现最简单的推送效果,只需要配置推送通道即可,后续可根据需要添加 Group 或者 User,甚至是 Client 相关的配置。

public static class LoggerConfigurationSignalRExtensions
{
    public static LoggerConfiguration SignalR(
        this LoggerSinkConfiguration loggerConfiguration,
        Uri uri,
        string channel,
        IFormatProvider formatProvider = null)
    {
        if (loggerConfiguration == null) 
             throw new ArgumentNullException(nameof(loggerConfiguration));
        if (uri == null) 
             throw new ArgumentNullException(nameof(uri));

        return loggerConfiguration.Sink(new SignalRSink(uri, channel, formatProvider));
    }
}

额外的支持

提供最基础的日志消息封装类 LogMessage,可根据需要进行扩展。

class LogMessage
{ 
    public string Name { get; set; }
    public string Level { get; set; }
    public string Message { get; set; }

    public LogMessage (string level, string message)
    {
        Level = level;
        Message = message;
    }
}

接收

使用 asp.net core 接收,对外提供 SignalR 接口和 Post 接口。

public class LogHub : Hub
{
    private readonly ILogger<LogHub> _logger;

    public LogHub(ILogger<LogHub> logger)
    {
        _logger = logger;
    }
    public async Task SendLog(LogMessage log)
    {
        Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}    {level}  =>  {log.Message}");

        await Clients.All.SendAsync("log", log);
    }
}

不要忘记添加 SignalR 支持

builder.Services.AddSignalR();

app.MapHub<LogHub>("/log");
app.MapPost("/test", async (IHubContext<LogHub> hub, [FromBody] LogMessage log) =>
    {
        Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}    {level}  =>  {log.Message}");
        await hub.Clients.All.SendAsync("log", log.Message);
        return log;
    })
    .WithName("testpost");

测试

新建一个项目,添加引用支持,通过如下配置,启动即可看到应用日志被推送到服务

var builder = WebApplication.CreateBuilder(args);

Log.Logger = new LoggerConfiguration()
    .Enrich.FromLogContext()
    .WriteTo.Console()
    .WriteTo.Http(new Uri("http://localhost:5268/test"))
    .WriteTo.SignalR(new Uri("http://localhost:5268/log"), "SendLog")
    .CreateLogger();

builder.Host.UseSerilog();

https://static.scung.cn/d1e0ad13-dd95-43f0-b61c-4c883937bbfd.png

发布时间:2022-04-04