程序日志处理的思考(C写日志工具类新版)
程序日志处理的思考(C写日志工具类新版)log4net.config配置文件:View Codeusing System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Utils { internal class LogStream { public FileStream CurrentFileStream { get; set; } public StreamWriter CurrentStreamWriter { get; set; } public int CurrentArchiveIndex { get; set; } p
昨天打算把我以前写的一个C#写日志工具类放到GitHub上,却发现了一个BUG,当然,已经修复了。
然后写Demo对比了NLog和log4net,发现我这个LogUtil比它们性能低了不止一个数量级(后来发现是通过共用Mutex修复BUG导致的)。工作多年,平时都是用别人写的库,自己写得很少。因为当初自己没有时间研究log4net或NLog,并且写个简单的日志工具类自己也有能力实现,所以就自己写了LogUtil自己用。修修改改了很多次了,居然还是有BUG。因为用了多线程和锁,导致BUG很隐蔽,而且性能比较差(后来发现是通过共用Mutex修复BUG导致的)。代码写的很挫,逻辑复杂,更容易出BUG。用NLog或log4net它不香吗?但又心有不甘,而且对于自己写的一些小的程序,可能第三方日志类库的dll比自己的程序都大,所以也有必要自己写一个,以便平时写各种Demo用。
日志类型LogType类:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Utils
{
/// <summary>
/// 日志类型
/// </summary>
public enum LogType
{
Debug
Info
Error
}
}
当前日志写入流LogStream类:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Utils
{
internal class LogStream
{
public FileStream CurrentFileStream { get; set; }
public StreamWriter CurrentStreamWriter { get; set; }
public int CurrentArchiveIndex { get; set; }
public long CurrentFileSize { get; set; }
public string CurrentDateStr { get; set; }
public string CurrentLogFilePath { get; set; }
public string CurrentLogFileDir { get; set; }
}
}
LogWriter类:
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
namespace Utils
{
internal class LogWriter
{
#region 字段属性
private LogType _logType;
private string _basePath;
private int _fileSize = 10 * 1024 * 1024; //日志分隔文件大小
private LogStream _currentStream = new LogStream();
private string _dateFormat = "yyyyMMdd"; //日志文件名日期格式化
private string _rootFolder = "Log"; //日志文件夹名称
private object _lockWriter = new object();
#endregion
#region LogWriter
public LogWriter(LogType logType)
{
_logType = logType;
Init();
}
#endregion
#region Init
/// <summary>
/// 初始化
/// </summary>
private void Init()
{
//初始化 _basePath
InitBasePath();
//创建目录
CreateLogDir();
//更新日志写入流
UpdateCurrentStream();
}
#endregion
#region 初始化 _basePath
/// <summary>
/// 初始化 _basePath
/// </summary>
private void InitBasePath()
{
UriBuilder uri = new UriBuilder(Assembly.GetExecutingAssembly().CodeBase);
_basePath = Path.GetDirectoryName(Uri.UnescapeDataString(uri.Path));
}
#endregion
#region 初始化 _currentArchiveIndex
/// <summary>
/// 初始化 _currentArchiveIndex
/// </summary>
private void InitCurrentArchiveIndex()
{
Regex regex = new Regex(_currentStream.CurrentDateStr "_*(\\d*).txt");
string[] fileArr = Directory.GetFiles(_currentStream.CurrentLogFileDir _currentStream.CurrentDateStr "*");
foreach (string file in fileArr)
{
Match match = regex.Match(file);
if (match.Success)
{
string str = match.Groups[1].Value;
if (!string.IsNullOrWhiteSpace(str))
{
int temp = Convert.ToInt32(str);
if (temp > _currentStream.CurrentArchiveIndex)
{
_currentStream.CurrentArchiveIndex = temp;
}
}
else
{
_currentStream.CurrentArchiveIndex = 0;
}
}
}
}
#endregion
#region 初始化 _currentFileSize
/// <summary>
/// 初始化 _currentFileSize
/// </summary>
private void InitCurrentFileSize()
{
FileInfo fileInfo = new FileInfo(_currentStream.CurrentLogFilePath);
_currentStream.CurrentFileSize = fileInfo.Length;
}
#endregion
#region CreateLogDir()
/// <summary>
/// 创建日志目录
/// </summary>
private void CreateLogDir()
{
string logDir = Path.Combine(_basePath _rootFolder "\\" _logType.Tostring());
if (!Directory.Exists(logDir))
{
Directory.CreateDirectory(logDir);
}
}
#endregion
#region CreateStream
/// <summary>
/// 创建日志写入流
/// </summary>
private void CreateStream()
{
_currentStream.CurrentFileStream = new FileStream(_currentStream.CurrentLogFilePath FileMode.Append FileAccess.Write FileShare.ReadWrite);
_currentStream.CurrentStreamWriter = new StreamWriter(_currentStream.CurrentFileStream Encoding.UTF8);
}
#endregion
#region CloseStream
/// <summary>
/// 关闭日志写入流
/// </summary>
private void CloseStream()
{
if (_currentStream.CurrentStreamWriter != null)
{
_currentStream.CurrentStreamWriter.Close();
}
if (_currentStream.CurrentFileStream != null)
{
_currentStream.CurrentFileStream.Close();
}
}
#endregion
#region 拼接日志内容
/// <summary>
/// 拼接日志内容
/// </summary>
private static string CreateLogString(LogType logType string log)
{
return string.Format(@"{0} {1} {2}" DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff") ("[" logType.ToString() "]").PadRight(7 ' ') log);
}
#endregion
#region 写文件
/// <summary>
/// 写文件
/// </summary>
private void WriteFile(string log)
{
try
{
lock (_lockWriter)
{
//判断是否更新Stream
string dateStr = DateTime.Now.ToString(_dateFormat);
if (_currentStream.CurrentDateStr != dateStr)
{
_currentStream.CurrentDateStr = dateStr;
UpdateCurrentStream();
}
//判断是否创建Archive
int byteCount = Encoding.UTF8.GetByteCount(log);
_currentStream.CurrentFileSize = byteCount;
if (_currentStream.CurrentFileSize >= _fileSize)
{
_currentStream.CurrentFileSize = 0;
CreateArchive();
}
//日志内容写入文件
_currentStream.CurrentStreamWriter.WriteLine(log);
_currentStream.CurrentStreamWriter.Flush();
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message "\r\n" ex.StackTrace);
}
}
#endregion
#region CreateArchive
/// <summary>
/// 创建日志存档
/// </summary>
private void CreateArchive()
{
string fileName = Path.GetFileNameWithoutExtension(_currentStream.CurrentLogFilePath);
CloseStream(); //关闭日志写入流
File.Move(_currentStream.CurrentLogFilePath Path.Combine(_currentStream.CurrentLogFileDir fileName "_" ( _currentStream.CurrentArchiveIndex) ".txt")); //存档
CreateStream(); //创建日志写入流
}
#endregion
#region UpdateCurrentStream
/// <summary>
/// 更新日志写入流
/// </summary>
private void UpdateCurrentStream()
{
try
{
//关闭日志写入流
CloseStream();
//创建新的日志路径
_currentStream.CurrentDateStr = DateTime.Now.ToString(_dateFormat);
_currentStream.CurrentLogFileDir = Path.Combine(_basePath _rootFolder "\\" _logType.ToString());
_currentStream.CurrentLogFilePath = Path.Combine(_currentStream.CurrentLogFileDir _currentStream.CurrentDateStr ".txt");
//创建日志写入流
CreateStream();
//初始化 _currentArchiveIndex
InitCurrentArchiveIndex();
//初始化 _currentFileSize
InitCurrentFileSize();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message "\r\n" ex.StackTrace);
}
}
#endregion
#region 写日志
/// <summary>
/// 写日志
/// </summary>
/// <param name="log">日志内容</param>
public void WriteLog(string log)
{
try
{
log = CreateLogString(_logType log);
WriteFile(log);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message "\r\n" ex.StackTrace);
}
}
#endregion
}
}
静态类LogUtil类:
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
namespace Utils
{
/// <summary>
/// 写日志类
/// </summary>
public class LogUtil
{
#region 字段
private static LogWriter _infoWriter = new LogWriter(LogType.Info);
private static LogWriter _DebugWriter = new LogWriter(LogType.Debug);
private static LogWriter _errorWriter = new LogWriter(LogType.Error);
#endregion
#region 写操作日志
/// <summary>
/// 写操作日志
/// </summary>
public static void Log(string log)
{
_infoWriter.WriteLog(log);
}
#endregion
#region 写调试日志
/// <summary>
/// 写调试日志
/// </summary>
public static void Debug(string log)
{
_debugWriter.WriteLog(log);
}
#endregion
#region 写错误日志
public static void Error(Exception ex string log = null)
{
Error(string.IsNullOrEmpty(log) ? ex.Message "\r\n" ex.StackTrace : (log ":") ex.Message "\r\n" ex.StackTrace);
}
/// <summary>
/// 写错误日志
/// </summary>
public static void Error(string log)
{
_errorWriter.WriteLog(log);
}
#endregion
}
}
测试代码(LogUtil、NLog、log4net写日志性能对比):
View Code
log4net.config配置文件:
View Code
NLog.config配置文件:
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.nlog-project.org/schemas/NLog.xsd NLog.xsd"
autoReload="true"
throwExceptions="false"
internalLogLevel="Off"
internalLogFile="d:\nlog\nlog-internal.log">
<!-- optional add some variables
https://github.com/nlog/NLog/wiki/Configuration-file#variables
-->
<!--<variable name="myvar" value="myvalue"/>-->
<variable name="logDir" value="${basedir}/nlog"/>
<variable name="logFileName" value="${date:format=yyyyMMdd}.txt"/>
<variable name="logArchiveFileName" value="${date:format=yyyyMMdd}_{#}.txt"/>
<variable name="logLayout" value="${date:format=yyyy-MM-dd HH\:mm\:ss.fff} [${level}] ${message}"/>
<!--
See https://github.com/nlog/nlog/wiki/Configuration-file
for information on customizing logging rules and outputs.
-->
<targets>
<!--
add your targets here
See https://github.com/nlog/NLog/wiki/Targets for possible targets.
See https://github.com/nlog/NLog/wiki/Layout-Renderers for the possible layout renderers.
-->
<!--
Write events to a file with the date in the filename.
<target xsi:type="File" name="f" fileName="${basedir}/logs/${shortdate}.log"
layout="${longdate} ${uppercase:${level}} ${message}" />
-->
<target xsi:type="File" name="info"
layout="${logLayout}"
fileName="${logDir}/info/${logFileName}"
archiveFileName="${logDir}/info/${logArchiveFileName}"
archiveAboveSize="10485760"
archiveNumbering="Sequence"
maxArchiveFiles="100"
concurrentWrites="true"
keepFileOpen="true"
openFileCacheTimeout="30"
encoding="UTF-8" />
<target xsi:type="File" name="debug"
layout="${logLayout}"
fileName="${logDir}/debug/${logFileName}"
archiveFileName="${logDir}/debug/${logArchiveFileName}"
archiveAboveSize="10485760"
archiveNumbering="Sequence"
maxArchiveFiles="100"
concurrentWrites="true"
keepFileOpen="true"
openFileCacheTimeout="30"
encoding="UTF-8" />
<target xsi:type="File" name="error"
layout="${logLayout}"
fileName="${logDir}/error/${logFileName}"
archiveFileName="${logDir}/error/${logArchiveFileName}"
archiveAboveSize="10485760"
archiveNumbering="Sequence"
maxArchiveFiles="100"
concurrentWrites="true"
keepFileOpen="true"
openFileCacheTimeout="30"
encoding="UTF-8" />
</targets>
<rules>
<!-- add your logging rules here -->
<!--
Write all events with minimal level of Debug (So Debug Info Warn Error and Fatal but not Trace) to "f"
<logger name="*" minlevel="Debug" writeTo="f" />
-->
<logger name="*" minlevel="Info" maxlevel="Info" writeTo="info" />
<logger name="*" minlevel="Debug" maxlevel="Debug" writeTo="debug" />
<logger name="*" minlevel="Error" maxlevel="Error" writeTo="error" />
</rules>
</nlog>
测试截图:
写Info、Debug、Error日志各30万行,LogUtil耗时4.628秒,NLog耗时4.900秒,log4net耗时10.564秒,硬盘是固态硬盘。
说明:
该版本不支持多进程并发。
支持多进程并发的LogWriter版本(注意:代码中要加上 _currentStream.CurrentFileStream.Seek(0 SeekOrigin.End); 这句,不然不支持多进程并发):
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
namespace Utils
{
/// <summary>
/// 支持多进程并发写日志的LogWriter版本
/// </summary>
internal class LogWriterUseMutex
{
#region 字段属性
private LogType _logType;
private string _basePath;
private int _fileSize = 10 * 1024 * 1024; //日志分隔文件大小
private LogStream _currentStream = new LogStream();
private string _dateFormat = "yyyyMMdd"; //日志文件名日期格式化
private string _rootFolder = "Log"; //日志文件夹名称
private Mutex _mutex;
#endregion
#region LogWriter
public LogWriterUseMutex(LogType logType)
{
_logType = logType;
_mutex = new Mutex(false "Mutex.LogWriter." logType.ToString() ".7693FFAD38004F6B8FD31F6A8B4CE2BD");
Init();
}
#endregion
#region Init
/// <summary>
/// 初始化
/// </summary>
private void Init()
{
//初始化 _basePath
InitBasePath();
//创建目录
CreateLogDir();
//更新日志写入流
UpdateCurrentStream();
}
#endregion
#region 初始化 _basePath
/// <summary>
/// 初始化 _basePath
/// </summary>
private void InitBasePath()
{
UriBuilder uri = new UriBuilder(Assembly.GetExecutingAssembly().CodeBase);
_basePath = Path.GetDirectoryName(Uri.UnescapeDataString(uri.Path));
}
#endregion
#region 初始化 _currentArchiveIndex
/// <summary>
/// 初始化 _currentArchiveIndex
/// </summary>
private void InitCurrentArchiveIndex()
{
Regex regex = new Regex(_currentStream.CurrentDateStr "_*(\\d*).txt");
string[] fileArr = Directory.GetFiles(_currentStream.CurrentLogFileDir _currentStream.CurrentDateStr "*");
foreach (string file in fileArr)
{
Match match = regex.Match(file);
if (match.Success)
{
string str = match.Groups[1].Value;
if (!string.IsNullOrWhiteSpace(str))
{
int temp = Convert.ToInt32(str);
if (temp > _currentStream.CurrentArchiveIndex)
{
_currentStream.CurrentArchiveIndex = temp;
}
}
else
{
_currentStream.CurrentArchiveIndex = 0;
}
}
}
}
#endregion
#region 初始化 _currentFileSize
/// <summary>
/// 初始化 _currentFileSize
/// </summary>
private void InitCurrentFileSize()
{
FileInfo fileInfo = new FileInfo(_currentStream.CurrentLogFilePath);
_currentStream.CurrentFileSize = fileInfo.Length;
}
#endregion
#region CreateLogDir()
/// <summary>
/// 创建日志目录
/// </summary>
private void CreateLogDir()
{
string logDir = Path.Combine(_basePath _rootFolder "\\" _logType.ToString());
if (!Directory.Exists(logDir))
{
Directory.CreateDirectory(logDir);
}
}
#endregion
#region CreateStream
/// <summary>
/// 创建日志写入流
/// </summary>
private void CreateStream()
{
_currentStream.CurrentFileStream = new FileStream(_currentStream.CurrentLogFilePath FileMode.Append FileAccess.Write FileShare.ReadWrite);
_currentStream.CurrentStreamWriter = new StreamWriter(_currentStream.CurrentFileStream Encoding.UTF8);
}
#endregion
#region CloseStream
/// <summary>
/// 关闭日志写入流
/// </summary>
private void CloseStream()
{
if (_currentStream.CurrentStreamWriter != null)
{
_currentStream.CurrentStreamWriter.Close();
}
if (_currentStream.CurrentFileStream != null)
{
_currentStream.CurrentFileStream.Close();
}
}
#endregion
#region 拼接日志内容
/// <summary>
/// 拼接日志内容
/// </summary>
private static string CreateLogString(LogType logType string log)
{
return string.Format(@"{0} {1} {2}" DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff") ("[" logType.ToString() "]").PadRight(7 ' ') log);
}
#endregion
#region 写文件
/// <summary>
/// 写文件
/// </summary>
private void WriteFile(string log)
{
try
{
_mutex.WaitOne();
//判断是否更新Stream
string dateStr = DateTime.Now.ToString(_dateFormat);
if (_currentStream.CurrentDateStr != dateStr)
{
_currentStream.CurrentDateStr = dateStr;
UpdateCurrentStream();
}
//判断是否创建Archive
int byteCount = Encoding.UTF8.GetByteCount(log);
_currentStream.CurrentFileSize = byteCount;
if (_currentStream.CurrentFileSize >= _fileSize)
{
_currentStream.CurrentFileSize = 0;
CreateArchive();
}
//日志内容写入文件
_currentStream.CurrentFileStream.Seek(0 SeekOrigin.End);
_currentStream.CurrentStreamWriter.WriteLine(log);
_currentStream.CurrentStreamWriter.Flush();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message "\r\n" ex.StackTrace);
}
finally
{
_mutex.ReleaseMutex();
}
}
#endregion
#region CreateArchive
/// <summary>
/// 创建日志存档
/// </summary>
private void CreateArchive()
{
string fileName = Path.GetFileNameWithoutExtension(_currentStream.CurrentLogFilePath);
CloseStream(); //关闭日志写入流
File.Move(_currentStream.CurrentLogFilePath Path.Combine(_currentStream.CurrentLogFileDir fileName "_" ( _currentStream.CurrentArchiveIndex) ".txt")); //存档
CreateStream(); //创建日志写入流
}
#endregion
#region UpdateCurrentStream
/// <summary>
/// 更新日志写入流
/// </summary>
private void UpdateCurrentStream()
{
try
{
//关闭日志写入流
CloseStream();
//创建新的日志路径
_currentStream.CurrentDateStr = DateTime.Now.ToString(_dateFormat);
_currentStream.CurrentLogFileDir = Path.Combine(_basePath _rootFolder "\\" _logType.ToString());
_currentStream.CurrentLogFilePath = Path.Combine(_currentStream.CurrentLogFileDir _currentStream.CurrentDateStr ".txt");
//创建日志写入流
CreateStream();
//初始化 _currentArchiveIndex
InitCurrentArchiveIndex();
//初始化 _currentFileSize
InitCurrentFileSize();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message "\r\n" ex.StackTrace);
}
}
#endregion
#region 写日志
/// <summary>
/// 写日志
/// </summary>
/// <param name="log">日志内容</param>
public void WriteLog(string log)
{
try
{
log = CreateLogString(_logType log);
WriteFile(log);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message "\r\n" ex.StackTrace);
}
}
#endregion
}
}
多进程并发的版本,性能差一些。
有BUG,File.Move这行代码多进程并发会异常,因文件一直是打开状态的,所以这种实现方式可能无法解决这个BUG。
总结:
新版本比旧版本代码逻辑更简单,代码组织更合理。
一个方法的代码行数不宜太长,逻辑要简单,这样不容易出BUG;单线程相比多线程,不容易出BUG。
自己写的代价很大,花了整整一天时间,用来练手没问题,但是不经过一两个项目的实际使用以验证没有BUG的话,你敢用吗?