前面几章介绍了 ASP.NET Core Logging 系统的配置和使用,而对于 Provider ,微软也提供了 Console, Debug, EventSource, TraceSource 等,但是没有我们最常用的 FilePrivider,而比较流行的 Log4Net , NLog 等也对 ASP.NET Core 的 Logging 系统提供了扩展,但是太过于复杂,而且他们本身就是一个完整的日志系统,功能上会有较多的重合,所以我们不妨自己动手,写一个轻量级的完全基于 ASP.NET Core Logging 系统的 FileProvider

IMessageWriter

首先定义一个日志写入接口:

public interface IMessageWriter : IDisposable
{
Task WriteMessagesAsync(string message, CancellationToken cancellationToken = default(CancellationToken));
}

只有一个异步的写日志方法,用来将日志写入到文件或者队列中。

FileWriter

IMessageWriter 最核心的实现,将日志写入到文件中。

public class FileWriter : IMessageWriter, IDisposable
{
... public FileWriter(string path, long? fileSizeLimit = null)
{
...
_underlyingStream = System.IO.File.Open(path, FileMode.Append, FileAccess.Write, FileShare.ReadWrite);
_output = new StreamWriter(_underlyingStream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
} public async Task WriteMessagesAsync(string message, CancellationToken cancellationToken)
{
if (_maxFileSize > 0 && _underlyingStream.Length > _maxFileSize)
{
return;
}
await _output.WriteAsync(message);
FlushToDisk();
} ...
}

其实现很简单,就是使用最基本的文件 Stream 来写入文件 ,并立即刷新到磁盘。

BatchingWriter

上面 FileWriter 最大的弊端就是每次写日志都要进行一次文件IO操作,效率较低,可以使用定时器,来定时刷新到磁盘,来提高性能。不过,在 Logging.AzureAppServices 中发现了更好的实现方式,即使用批量提交:

public class BatchingWriter : IMessageWriter, IDisposable
{
... public BatchingWriter(IMessageWriter writer, TimeSpan interval, int? batchSize, int? queueSize)
{
...
Start();
} private void Start()
{
_messageQueue = _queueSize == null ?
new BlockingCollection<string>(new ConcurrentQueue<string>()) :
new BlockingCollection<string>(new ConcurrentQueue<string>(), _queueSize.Value); _cancellationTokenSource = new CancellationTokenSource();
_outputTask = Task.Factory.StartNew<Task>(
ProcessLogQueue,
null,
TaskCreationOptions.LongRunning);
} private async Task ProcessLogQueue(object state)
{
StringBuilder currentBatch = new StringBuilder();
while (!_cancellationTokenSource.IsCancellationRequested)
{
var limit = _batchSize ?? int.MaxValue;
while (limit > 0 && _messageQueue.TryTake(out var message))
{
currentBatch.Append(message);
limit--;
}
if (currentBatch.Length > 0)
{
try
{
await _writer.WriteMessagesAsync(currentBatch.ToString(), _cancellationTokenSource.Token);
}
catch
{
// ignored
}
}
await IntervalAsync(_interval, _cancellationTokenSource.Token);
}
} protected virtual Task IntervalAsync(TimeSpan interval, CancellationToken cancellationToken)
{
return Task.Delay(interval, cancellationToken);
} private void Stop()
{
...
} public Task WriteMessagesAsync(string message, CancellationToken cancellationToken)
{
if (!_messageQueue.IsAddingCompleted)
{
try
{
_messageQueue.Add(message, _cancellationTokenSource.Token);
}
catch
{
//cancellation token canceled or CompleteAdding called
}
}
return Task.CompletedTask;
} ...
}

首先定义了一个并发队列,每次写入只需要将日志保存到队列当中,通过配置获取执行周期来定期从队列中取出日志,再使用上面的 FileWriter 来持久化到磁盘。

RollingFileWriter

使用上面两个类,已满足了最基本的写日志功能,但是在 Log4Net 等日志框架中,我们经常会按一定的频度滚动日志记录文件,也就是 RollingFile 功能,可实现将每天或每小时的日志保存到一个文件中,按文件大小进行滚动等功能。

首先是定义了一个 RollingFrequency 类,用来根据配置的文件名,来获取滚动频率,比如我们指定日志文件名为 Logs\my-{Date}.log,则表示每天滚动一次。

public class RollingFrequency
{
public static readonly RollingFrequency Date = new RollingFrequency("Date", "yyyyMMdd", TimeSpan.FromDays(1));
public static readonly RollingFrequency Hour = new RollingFrequency("Hour", "yyyyMMddHH", TimeSpan.FromHours(1)); public string Name { get; }
public string Format { get; }
public TimeSpan Interval { get; } RollingFrequency(string name, string format, TimeSpan interval)
{
if (name == null) throw new ArgumentNullException(nameof(name));
Format = format ?? throw new ArgumentNullException(nameof(format));
Name = "{" + name + "}";
Interval = interval;
} public DateTime GetCurrentCheckpoint(DateTime instant)
{
if (this == Hour)
{
return instant.Date.AddHours(instant.Hour);
}
return instant.Date;
} public DateTime GetNextCheckpoint(DateTime instant) => GetCurrentCheckpoint(instant).Add(Interval); public static bool TryGetRollingFrequency(string pathTemplate, out RollingFrequency specifier)
{
if (pathTemplate == null) throw new ArgumentNullException(nameof(pathTemplate));
var frequencies = new[] { Date, Hour }.Where(s => pathTemplate.Contains(s.Name)).ToArray();
specifier = frequencies.LastOrDefault();
return specifier != null;
}
}

再看一下 RollingFileWriter

public class RollingFileWriter : IMessageWriter, IDisposable
{
... public RollingFileWriter(string pathFormat, long? fileSizeLimitBytes = null, int? retainedFileCountLimit = null)
{
...
} public Task WriteMessagesAsync(string message, CancellationToken cancellationToken)
{
AlignFileWriter();
return _currentFileWriter.WriteMessagesAsync(message, cancellationToken);
} private void AlignFileWriter()
{
DateTime now = DateTime.Now;
if (!_nextCheckpoint.HasValue)
{
OpenFileWriter(now);
}
else if (now >= _nextCheckpoint.Value)
{
CloseFileWriter();
OpenFileWriter(now);
}
} private void OpenFileWriter(DateTime now)
{
var currentCheckpoint = _roller.GetCurrentCheckpoint(now);
_nextCheckpoint = _roller.GetNextCheckpoint(now); var existingFiles = Enumerable.Empty<string>();
try
{
existingFiles = Directory.GetFiles(_roller.LogFileDirectory, _roller.FileSearchPattern).Select(Path.GetFileName);
}
catch (DirectoryNotFoundException) { } var latestForThisCheckpoint = _roller
.SelectMatches(existingFiles)
.Where(m => m.DateTime == currentCheckpoint)
.OrderByDescending(m => m.SequenceNumber)
.FirstOrDefault(); var sequence = latestForThisCheckpoint != null ? latestForThisCheckpoint.SequenceNumber : 0; const int maxAttempts = 3;
for (var attempt = 0; attempt < maxAttempts; attempt++)
{
string path = _roller.GetLogFilePath(now, sequence);
try
{
_currentFileWriter = new FileWriter(path, _maxfileSizeLimit);
}
catch (IOException)
{
sequence++;
continue;
}
RollFiles(path);
return;
}
} // 删除超出保留文件数的日志文件
private void RollFiles(string currentFilePath)
{
if (_maxRetainedFiles > 0)
{
var potentialMatches = Directory.GetFiles(_roller.LogFileDirectory, _roller.FileSearchPattern)
.Select(Path.GetFileName);
var moveFiles = _roller
.SelectMatches(potentialMatches)
.OrderByDescending(m => m.DateTime)
.ThenByDescending(m => m.SequenceNumber)
.Skip(_maxRetainedFiles.Value)
.Select(m => m.Filename);
foreach (var obsolete in moveFiles)
{
System.IO.File.Delete(Path.Combine(_roller.LogFileDirectory, obsolete));
}
}
} ...
}

根据滚动频率指定应该创建的文件名,然后调用 FileWriter 进行写入,具体代码可以去看文末贴的 GitHub 地址。

FileLogger

FileLogger 则是由上一章讲到的 Logger 来调用的,而在这里,它的作用是首先对日志进行过滤,然后将日志组装成字符串,再调用我们前面定义的 IMessageWriter 进行日志的写入:

public class FileLogger : ILogger, IDisposable
{
... public FileLogger(IMessageWriter writer, string category, Func<string, LogLevel, bool> filter)
{
...
} public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
if (!IsEnabled(logLevel))
{
return;
}
if (formatter == null)
{
throw new ArgumentNullException(nameof(formatter));
}
var builder = new StringBuilder();
builder.Append(DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss.fff zzz"));
builder.Append(" [");
builder.Append(GetLogLevelString(logLevel));
builder.Append("] ");
builder.Append(_category);
builder.Append("[");
builder.Append(eventId);
builder.Append("]");
builder.Append(": ");
builder.AppendLine(formatter(state, exception));
if (exception != null)
{
builder.AppendLine(exception.ToString());
}
_writer.WriteMessagesAsync(builder.ToString()).Wait();
} ...
}

在这里,日志的拼装是写死的,后续可以提供一个可配置的日志渲染器,来自定义输出格式。

ConsoleLoggerProvider

FileLoggerProvider 的唯一职责就是创建 FileLogger

[ProviderAlias("File")]
public class FileLoggerProvider : ILoggerProvider
{
... public FileLoggerProvider(IOptionsMonitor<FileLoggerOptions> options)
{
_optionsChangeToken = options.OnChange(UpdateOptions);
UpdateOptions(options.CurrentValue);
} private void UpdateOptions(FileLoggerOptions options)
{
if (RollingFrequency.TryGetRollingFrequency(options.Path, out var r))
{
_msgWriter = new RollingFileWriter(options.Path, options.FileSizeLimit, options.RetainedFileCountLimit);
}
else
{
_msgWriter = new FileWriter(options.Path, options.FileSizeLimit);
}
if (options.IsEnabledBatching)
{
_msgWriter = new BatchingWriter(_msgWriter, options.FlushPeriod, options.BatchSize, options.BackgroundQueueSize);
}
} public ILogger CreateLogger(string categoryName)
{
return new FileLogger(_msgWriter, categoryName, _filter);
} ...
}

首先是 ProviderAlias 特性,为 Provider 指定一个别名,这样,我们在配置文件中指定 Provider 时,使用别名即可,然后使用了 IOptionsMonitor 模式,监控配置的变化,并进行更新,而不用去重启Web服务器。

FileLoggerFactoryExtensions

最后便是提供扩展方法,方便我们在 Program 中对日志系统进行配置。而扩展方法的实现只是很简单的将我们定义的 FileProvider 注入进去:

public static class FileLoggerFactoryExtensions
{
... public static ILoggingBuilder AddFile(this ILoggingBuilder builder, IConfiguration configuration)
{
builder.Services.AddSingleton<IOptionsChangeTokenSource<LoggerFilterOptions>>(new ConfigurationChangeTokenSource<LoggerFilterOptions>(configuration));
builder.Services.Configure<FileLoggerOptions>(configuration);
builder.Services.AddSingleton<IConfigureOptions<FileLoggerOptions>>(new FileLoggerConfigureOptions(configuration));
builder.AddFile();
return builder;
} ...
}

只提供了 ILoggingBuilder 的扩展,而不再提供 ILoggerFactory 的扩展方法,全力拥抱 .NET Core 2.0

总结

通过对网上各种流行的开源日志框架学习借鉴,写了一个 ASP.NET Core 的 Logging 系统的文件扩展,还有很多不足之处,但更多的是一种探索,学习,借此也对 Logging 系统更加了解。而后续会再研究一下分布式日记系统。

最后附上本文所示代码地址:zero-logging

ASP.NET Core 源码学习之 Logging[4]:FileProvider的更多相关文章

  1. ASP.NET Core 源码学习之 Logging[1]:Introduction

    在ASP.NET 4.X中,我们通常使用 log4net, NLog 等来记录日志,但是当我们引用的一些第三方类库使用不同的日志框架时,就比较混乱了.而在 ASP.Net Core 中内置了日志系统, ...

  2. ASP.NET Core 源码学习之 Logging[2]:Configure

    在上一章中,我们对 ASP.NET Logging 系统做了一个整体的介绍,而在本章中则开始从最基本的配置开始,逐步深入到源码当中去. 默认配置 在 ASP.NET Core 2.0 中,对默认配置做 ...

  3. ASP.NET Core 源码学习之 Logging[3]:Logger

    上一章,我们介绍了日志的配置,在熟悉了配置之后,自然是要了解一下在应用程序中如何使用,而本章则从最基本的使用开始,逐步去了解去源码. LoggerFactory 我们可以在构造函数中注入 ILogge ...

  4. 【ASP.NET Core 】ASP.NET Core 源码学习之 Logging[1]:Introduction

    在ASP.NET 4.X中,我们通常使用 log4net, NLog 等来记录日志,但是当我们引用的一些第三方类库使用不同的日志框架时,就比较混乱了.而在 ASP.Net Core 中内置了日志系统, ...

  5. ASP.NET Core源码学习(一)Hosting

    ASP.NET Core源码的学习,我们从Hosting开始, Hosting的GitHub地址为:https://github.com/aspnet/Hosting.git 朋友们可以从以上链接克隆 ...

  6. ASP.NET Core 源码学习之 Options[1]:Configure

    配置的本质就是字符串的键值对,但是对于面向对象语言来说,能使用强类型的配置是何等的爽哉! 目录 ASP.NET Core 配置系统 强类型的 Options Configure 方法 源码解析 ASP ...

  7. ASP.NET Core 源码学习之 Options[4]:IOptionsMonitor

    前面我们讲到 IOptions 和 IOptionsSnapshot,他们两个最大的区别便是前者注册的是单例模式,后者注册的是 Scope 模式.而 IOptionsMonitor 则要求配置源必须是 ...

  8. asp.net core源码飘香:Logging组件

    简介: 作为基础组件,日志组件被其他组件和中间件所使用,它提供了一个统一的编程模型,即不需要知道日志最终记录到哪里去,只需要调用它即可. 使用方法很简单,通过依赖注入ILogFactory(Creat ...

  9. ASP.NET Core 源码学习之 Options[2]:IOptions

    在上一篇中,介绍了一下Options的注册,而使用时只需要注入IOption即可: public ValuesController(IOptions<MyOptions> options) ...

随机推荐

  1. Linux下memcached安装与连接

    前几天技术总监要我在项目中加一个memcached,以前也从来没有配置过,所以就去网上找教程,最终折腾成功.比较坑的就是sasl协议那里. 由于memcached依赖libevents,所以要下载两个 ...

  2. Vue和Bootstrap的整合之路

    我是一个刚刚接触前端开发的新手,所以有必要记录如何将Bootstrap和Vue进行整合. 如果你是老手,请直接绕道而过.作为一个新手,里面的步骤,过程或者专业术语未必正确,如果你发现哪里错误了,请发邮 ...

  3. Redis 内存管理与事件处理

    1 Redis内存管理 Redis内存管理相关文件为zmalloc.c/zmalloc.h,其只是对C中内存管理函数做了简单的封装,屏蔽了底层平台的差异,并增加了内存使用情况统计的功能. void * ...

  4. 基于邮件系统的远程实时监控系统的实现 Python版

    人生苦短,我用Python~ 界内的Python宣传标语,对Python而言,这是种标榜,实际上,Python确实是当下最好用的开发语言之一. 在相继学习了C++/C#/Java之后,接触Python ...

  5. react系列从零开始-react介绍

    react算是目前最火的js MVC框架了,写一个react系列的博客,顺便回忆一下react的基础知识,新入门前端的小白,可以持续关注,我会从零开始教大家用react开发一个完整的项目,也会涉及到w ...

  6. View学习(三)- View的布局(layout)过程

    前段开始学习View的工作原理,前两篇博客的草稿都已经写好了,本想一鼓作气写完所有的相关文章,然后经历了一段连续加班,结果今天准备继续写文章时,把之前写好的东西都忘记了,又重新梳理了一遍,所以说那怕就 ...

  7. Unity3D-Shader-复古电影荧幕特效

    [旧博客转移 - 2015年12月6日 18:12]   今天用Shader做了一个复古荧幕效果,老电视机放映的感觉,写篇文章记录一下     原始图片:   没错,这就是电影<泰坦尼克号> ...

  8. Android后门GhostCtrl,完美控制设备任意权限并窃取用户数据

    Android系统似乎已经成为世界各地病毒作者的首选目标,每天都有新的恶意软件在感染更多的设备. 这一次,安全公司趋势科技发布警告,他们发现了一个新的Android后门--GhostCtrl Ghos ...

  9. Java设计模式之包装模式

    有时候一个对象的方法可能不是我们想要的功能,我们希望能将这个方法覆写.而对于覆写,我们最直白的感觉就是通过子类继承的方式,但是有时候对于使用web开发而言,我们能知道获取对象的实现接口,而真正对象是属 ...

  10. 008.Adding a model to an ASP.NET Core MVC app --【在 asp.net core mvc 中添加一个model (模型)】

    Adding a model to an ASP.NET Core MVC app在 asp.net core mvc 中添加一个model (模型)2017-3-30 8 分钟阅读时长 本文内容1. ...