Github源码
第一篇介绍如何使用Trace
记录日志
在MVC中使用 blqw.Logger(1)
第二篇介绍如何使用TraceSource
记录日志
在MVC中使用 blqw.Logger(2)
第三篇介绍如何切换侦听器和自定义侦听器
在MVC中使用 blqw.Logger(3)
这一篇主要介绍一下整个Logger的设计
首先 本组件是基于微软的 System.Diagnostics.TraceListener 实现的
日志的入口依然是 Debug, Trace, TraceSource
基于 System.Diagnostics.TraceListener 的 TraceListenerBase
整个组件的核心就是 自定义了一个 TraceListenerBase
这个类的主要作用是:
- 确定写入器( IWriter )
写入器功能是将日志输出或持久化,不同的写入器,可以以不同的方式输出日志 (* 写入文件, 写入系统时间查看器 )
不同的写入器也可以控制以不同的格式输出日志 ( SLSWriter,FastFileWriter *) - 实例化日志队列 WriteQueue (* 实例化时需要传入写入器 *)
日志队列主要用于临时缓存日志对象,然通过单例任务 SingletonTask 控制并发,有序的调用写入器的Append
,Flush
等方法输出日志;
( _ 单例任务还会判断调用写入器的方法是否超时,超时终止,终止后重启,无数据时退出等 _ ) - 将日志转为指定的格式 LogItem, 并送入日志队列
这里还会获取上下文 LoggerContext,上下文的作用主要是为了保证同一次操作的日志id都相同;
有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个公开的成员
- 构造函数
传入超时时间,用于确定一个任务的最长执行时间 - 超时时间
Timeout
由于构造函数传入,只读 - 是否正在运行
IsRunning
判断任务是否正在运行 - 单例任务
event AsyncEventHandler OnRun
- 运行任务
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 或者别的地方
将抓取程序独立出来也是为了发送日志不影响应用程序
完整流程图
嗯。。其他就没什么特别的了,如果还有问题可以留言告诉我。