在MVC中使用 blqw.Logger(4)

Github源码

第一篇介绍如何使用Trace记录日志
在MVC中使用 blqw.Logger(1)
第二篇介绍如何使用TraceSource记录日志
在MVC中使用 blqw.Logger(2)
第三篇介绍如何切换侦听器和自定义侦听器
在MVC中使用 blqw.Logger(3)

这一篇主要介绍一下整个Logger的设计

首先 本组件是基于微软的 System.Diagnostics.TraceListener 实现的
日志的入口依然是 Debug, Trace, TraceSource

在MVC中使用 blqw.Logger(4)_第1张图片

基于 System.Diagnostics.TraceListener 的 TraceListenerBase

整个组件的核心就是 自定义了一个 TraceListenerBase
这个类的主要作用是:

  1. 确定写入器( IWriter )
    写入器功能是将日志输出或持久化,不同的写入器,可以以不同的方式输出日志 (* 写入文件, 写入系统时间查看器
    不同的写入器也可以控制以不同的格式输出日志 (
    SLSWriter,FastFileWriter *)
  2. 实例化日志队列 WriteQueue (* 实例化时需要传入写入器 *)
    日志队列主要用于临时缓存日志对象,然通过单例任务 SingletonTask 控制并发,有序的调用写入器的AppendFlush等方法输出日志;
    ( _ 单例任务还会判断调用写入器的方法是否超时,超时终止,终止后重启,无数据时退出等 _ )
  3. 将日志转为指定的格式 LogItem, 并送入日志队列
    这里还会获取上下文 LoggerContext,上下文的作用主要是为了保证同一次操作的日志id都相同;
在MVC中使用 blqw.Logger(4)_第2张图片

有2个设计在这里分享下

上下文

我之前在日志的时候经常会这样的问题:

private static string[] configs = new string[9];

static void Main(string[] args)
{
    for (int i = 0; i < 10; i++)
        MethodA(i);
}


private static void MethodA(int i)
{
    Trace.WriteLine(i, "MethodA:i");
    MethodB(configs[i]);
}

private static void MethodB(string s)
{
    try
    {
        Console.WriteLine(s.IndexOf("0"));
    }
    catch (Exception e)
    {
        Trace.WriteLine(s, "MethodB:s");
        Trace.WriteLine(e, "MethodB:Exception");
        throw;
    }
}

A和B都写日志了,但是当B失败的时候无法找到对应的A方法中的参数是什么值,所以就有了上下文
将logid保存在上下文中,这样就可以在同一次调用时使用相同的logid,根据logid相同就吧同时方法中输出的日志串联起来

private bool Initialize(bool create = true)
{
    if (_isInitialized == false)
    {
        _values = (object[])(CallContext.LogicalGetData(CONTEXT_FIELD) ?? HttpContext.Current?.Items[CONTEXT_FIELD]);
        if (_values != null)
        {
            _contextID = (Guid)_values[0];
            _minLevel = (TraceEventType)_values[1];
            _isNew = false;
        }
        else if (create)
        {
            _contextID = Trace.CorrelationManager.ActivityId;
            _minLevel = 0;
            _isNew = true;
            if (_contextID == Guid.Empty)
            {
                Trace.CorrelationManager.ActivityId = _contextID = Guid.NewGuid();
            }
            _values = new object[] { _contextID, _minLevel };
            CallContext.LogicalSetData(CONTEXT_FIELD, _values);
            HttpContext.Current?.Items.Add(CONTEXT_FIELD, _values);
        }
        else
        {
            return false;
        }
        _isInitialized = true;
    }
    return true;
}

单例任务管理器

这个对象主要的作用是管理一个任务,保证这个任务同一时间只有一个在运行
5个公开的成员

  1. 构造函数
    传入超时时间,用于确定一个任务的最长执行时间
  2. 超时时间 Timeout
    由于构造函数传入,只读
  3. 是否正在运行 IsRunning
    判断任务是否正在运行
  4. 单例任务 event AsyncEventHandler OnRun
  5. 运行任务 RunIfStop
    判断任务是否在运行,如果不在运行则激活任务,如果任务已经在运行则无任何作用

下面示例的代码我会删除与主逻辑无关的部分代码,使主逻辑更清晰,想看网站的移步Github

在队列初始化时候,会构造一个单例任务对象

private readonly ConcurrentQueue _items;
private readonly IWriter _writer;
private readonly SingletonTask _task;
public WriteQueue(IWriter writer)
{
    if (writer == null) throw new ArgumentNullException(nameof(writer));
    _items = new ConcurrentQueue();
    _writer = writer;
    _task = new SingletonTask(30); //30秒无活动则任务超时
    _task.OnRun += WriteAsync; //绑定任务
}

在任务队列中每次有新任务加入,都会调用RunIfStop方法

public void Add(LogItem item)
{
    _items.Enqueue(item);
    _task.RunIfStop();
}
private ActivityTokenSource _taskToken;
private DateTime _lastRunTime;
public void RunIfStop()
{
    if (IsRunning)
    {
        return;
    }
    var token = new ActivityTokenSource(Timeout); 
    if (Interlocked.CompareExchange(ref _taskToken, token, null) != null)
    {
        return;
    }
    SynchronizationContext.SetSynchronizationContext(null);
    Run(token).ConfigureAwait(false);
}
public bool IsRunning
{
    get
    {
        var b = _taskToken?.CancelIfTimeout(); //如果超时,取消任务
        if (b == null)
        {
            return false;
        }
        var interval = (DateTime.Now - _lastRunTime).TotalSeconds;
        if (interval < _checkInterval)
        {
            return b.Value;
        }
        _lastRunTime = DateTime.Now;
        Interlocked.MemoryBarrier();
        var value = _taskToken?.CancelIfTimeout() ?? false;
        return value;
    }
}
public event AsyncEventHandler OnRun;
private async Task Run(ActivityTokenSource token)
{
    await Task.Delay(1);
    try
    {
        var task = OnRun?.Invoke(token);
        if (task == null) return;
        _lastRunTime = DateTime.Now;
        await task;
    }
    finally
    {
        Interlocked.CompareExchange(ref _taskToken, null, token);
    }
}

单例任务控制的方法,就是将队列中的数据取出,然后调用IWriter.Append

private async Task WriteAsync(ActivityTokenSource tokenSource)
{
    while (true)
    {
        tokenSource.Activity(); //表示一次活动,如果任务已经取消,这里抛出异常
        LogItem log;
        //队列中没有对象
        if (_items.TryDequeue(out log) == false)
        {
            return;
        }

        var @async = _writer as IAppendAsync;
        if (@async == null)
        {
            _writer.Append(log);
        }
        else
        {
            var task = @async.AppendAsync(log, tokenSource.Token);
            if (task != null)
            {
                await task.ConfigureAwait(false);
                if (task.Exception != null) throw task.Exception;
            }
        }
    }
}

这里有一个 ActivityTokenSource tokenSource继承自CancellationTokenSource
),这个信号是由SingletonTask传入的,主要用于确定任务是否还在活动,所以每次循环都需要调用一下tokenSource.Activity();该方法会刷新最后活动时间,如果调用时距离上次调用的时间已经超过了设定的超时时间,则这个方法会抛出异常

public DateTime LastActivityTime { get; private set; }
public void Activity()
{
    ThrowIfCancellationRequested();
    LastActivityTime = DateTime.Now;
}
private void ThrowIfCancellationRequested()
{
    if (CancelIfTimeout())
    {
        throw new OperationCanceledException($"任务因 {_timeout} 无动作被取消", Token);
    }
}
public bool CancelIfTimeout()
{
    if (IsCancellationRequested || Token.IsCancellationRequested)
    {
        return true;
    }

    if (DateTime.Now - LastActivityTime < _timeout)
    {
        return false;
    }
    Cancel();
    return true;
}

在实际的应用中

  • 日志输出到本地文件
    为了更快的输出速度,不影应用程序
  • 组件会主动删除3天前的文件
    为了保证磁盘的可用,当然这个值是可以设置的
  • 后续通过logstash 将日志文件抓到 kafka 或者别的地方
    将抓取程序独立出来也是为了发送日志不影响应用程序

完整流程图

在MVC中使用 blqw.Logger(4)_第3张图片
image.png

嗯。。其他就没什么特别的了,如果还有问题可以留言告诉我。

你可能感兴趣的:(在MVC中使用 blqw.Logger(4))