博客 / 詳情

返回

[Maui] 造輪子——LoggerSqlite

上文異常處理用到了日誌記錄器,本文介紹一下基於Sqlite的日誌記錄器

一、定義一個傳遞、保存日誌的類

public class LogItem
{
    public int Id { get; set; }
    public string DT { get; set; } = default!;
    public LogLevel Level { get; set; }
    public string CategoryName { get; set; } = default!;
    public string Message { get; set; } = default!;
    public string Trace { get; set; } = default!;
    public Exception? Exception { get; set; }
    public string Details => $"""
        ============={CategoryName}======================
        日期:{DT}
        {Message}
        """;


    public bool IsVisible => !string.IsNullOrEmpty(Trace);

    public LogItem() { }
    public LogItem(LogLevel level, string categoryName, string message, Exception? e)
    {
        DT = DateTime.Now.ToString();
        Level = level;
        CategoryName = categoryName;
        Exception = e;
    }

}

二、自定義日誌記錄器

internal class SqliteLogger(string name, ChannelWriter<LogItem> writer) : ILogger
{
    public IDisposable? BeginScope<TState>(TState state) where TState : notnull => default!;
    public bool IsEnabled(LogLevel logLevel) => logLevel > LogLevel.Debug;
    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, System.Exception? exception, Func<TState, System.Exception?, string> formatter)
    {
        if (!IsEnabled(logLevel))
        {
#if DEBUG
            Console.WriteLine(logLevel + ":" + name + "," + formatter(state, exception));
#endif
            return;
        }
        writer.TryWrite(new LogItem(logLevel, name, formatter(state, exception), exception));
    }
} 

三、自定義日誌記錄器提供程序

internal class SqliteLoggerProvider : ILoggerProvider
{
    private readonly ConcurrentDictionary<string, SqliteLogger> _loggers = new(StringComparer.OrdinalIgnoreCase);
    private readonly CancellationTokenSource _Cts = new();
    private readonly Channel<LogItem> _Queue;
    private readonly ChannelWriter<LogItem> _Writer;
    private readonly string _DBConnectionString;
    public SqliteLoggerProvider(IConfiguration config)
    {
        var dbName = config["LogDatabaseName"];
        _DBConnectionString = @$"Data Source={dbName}";
        var dbPath = Path.GetDirectoryName(dbName)!;
        if (!Directory.Exists(dbPath))
            Directory.CreateDirectory(dbPath);

        var option = new UnboundedChannelOptions
        {
            SingleReader = true
        };
        _Queue = Channel.CreateUnbounded<LogItem>(option);
        _Writer = _Queue.Writer;
        var reader = _Queue.Reader;


        Task.Run(async () =>
        {
            var cn = new SqliteConnection(_DBConnectionString);
            var cmd = cn.CreateCommand();
            cmd.CommandText = """
            create table if not exists LogItem (
                Id INTEGER PRIMARY KEY  AUTOINCREMENT not null,
                DT TEXT not null,
                Level int not null,
                CategoryName nvarchar(128) not null,
                Message TEXT not null,
                Trace TEXT not null
            )
            """;
            await cn.OpenAsync();
            await cmd.ExecuteNonQueryAsync();
            await cn.CloseAsync();


            cmd.CommandText = $"""
            insert into {nameof(LogItem)}(
                {nameof(LogItem.DT)},
                {nameof(LogItem.Level)},
                {nameof(LogItem.CategoryName)},
                {nameof(LogItem.Message)},
                {nameof(LogItem.Trace)}
                )
            values(@P0,@P1,@P2,@P3,@P4)
            """;
            cmd.Parameters.AddWithValue("@P0", null);
            cmd.Parameters.AddWithValue("@P1", null);
            cmd.Parameters.AddWithValue("@P2", null);
            cmd.Parameters.AddWithValue("@P3", null);
            cmd.Parameters.AddWithValue("@P4", null);

            try
            {
                while (await reader.WaitToReadAsync(_Cts.Token))
                {
                    await cn.OpenAsync();
                    while (reader.TryRead(out var item))
                    {
                        if (item.Exception is null)
                        {
                            item.Trace = "";
                        }
                        else
                        {

                            item.Message = (string.IsNullOrEmpty(item.Message) ? "" : (item.Message + "\r\n")) + item.Exception.GetFriendMessage();
                            item.Trace = System.Text.Json.JsonSerializer.Serialize(VMTrace.CreateList(item.Exception.StackTrace));
                        }

                        cmd.Parameters[0].Value = DateTime.Now;
                        cmd.Parameters[1].Value = (int)item.Level;
                        cmd.Parameters[2].Value = item.CategoryName;
                        cmd.Parameters[3].Value = item.Message;
                        cmd.Parameters[4].Value = item.Trace;
                        await cmd.ExecuteNonQueryAsync();
                    }
                    await cn.CloseAsync();
                }
            }
            catch (System.Exception ex)
            {
                Debug.WriteLine(ex.Message);
            }
        });
    }
    public ILogger CreateLogger(string categoryName) =>
      _loggers.GetOrAdd(categoryName, name => new SqliteLogger(name, _Writer));
    public void Dispose()
    {
        _Cts.Cancel();
        _loggers.Clear();
    }
}

這個提供程序順便把日誌寫入數據庫的活給幹了。

這裏表揚一下Channel這個東東,它的Reader比較好,一直讀到隊列為空,然後等待,實在是比較窩心。

四、總結一下

  1. 日誌的消費者(領導)

    • 給我弄個記錄器
      public class SomeClass(ILogger<SomeClass> logger)
      {
      }
      
      public void SomeMethod()
      {
          var logger=Handler.GetRequiredService<ILogger<T>>();
          或者
          Handler.GetRequiredService<ILoggerFactory>().CreateLogger("CategogyName");
      }
      
    • 我要寫點日誌
  2. 日誌工廠ILoggerFactory (廠長)
    廠長一通忙乎,催着手下的ILoggerProvider

    • 快快快,整個記錄器,快快快領導要講話了,是什麼類型的
    • 快快快,領導講的話記下來!!!
  3. 日誌供應商ILoggerProvider(車間主任)

    • 給你一個記錄器,就是領導要的類型的
  4. 日誌記錄器ILogger(產品)

    • 我就記一下,我就記這一類的,其他事別煩我
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.