通过编写一个简单的日志类库来加深了解C#的文件访问控制

在程序的开发调试过程及发布运行后的状态监控中,日志都有着极其重要的分量,通过在关键逻辑节点将关键数据记录到日志文件当中能帮助我们尽快找到程序问题所在。网上有不少专业成熟的日志组件可用,比如log4net和nlog等,由其专业及受欢迎程度可见日志在一个程序中的重要性。

我只用过log4net,而在用log4net写日志的过程中慢慢觉着太繁琐了点,不就写个日志吗?为毛搞得那么复杂?各种配置让我有点抓狂。

于是我就想,自己来吧!

首先分析一下一个基本的日志类库应该具有的基本功能及实现的时候需要注意和解决的问题:

1.关于日志文件的写入

写日志并不是简单的打开一个文件然后写入数据然后关闭了事,无论是web程序还是桌面程序,首要问题是多线程争抢写入一个日志文件的访问控制,次要问题是要允许其它进程在写入进程未释放日志文件时日志文件能被读取——试想如果日志在写的时候不能被读取那日志将毫无价值。

为了解决多线程写入的问题,多线程写入的数据将被缓存在一个StringBuilder对象中,而后由一个专门的写文件线程来负责取出数据写入到日志文件,以此来保证只有一个线程对日志文件进行写操作,如果再解决在文件流未关闭的情况下让其它进程或线程能读取日志内容,那问题就都不是问题了,而在文件流未关闭的情况下要让其它进程或线程能读取日志内容只需要在打开或创建日志文件的FileStream时指定System.IO.FileShare参数为Read即可。

2.关于日志文件的读取

文件写入成功后会有读取进行查看及分析的需求。内容较少的时候直接记事本打开即可,但是日志较大的时候就费劲了,虽然也有一些专门的软件能打开大文本文件,可打开日志文件有时并不是只为了看上一眼而已,很可能需要提取一些受关注的数据做个统计分析,比如提取某个操作的耗时来做瓶颈参考,因此有必要实现对大文本文件的读取,在读取过程中进行数据的留存分析。

对大文本文件的读取当然要按块来读取,比如一次读取10M字节,这样即便是几个G的文件也没几次可读的,重要的是不能截断单词和宽字符,所以每读取到指定字节数(如10M字节)的数据后需要根据指定的参考字符(如换行符、空格、逗号、句号等)做偏移计算。

对文件的读取在创建文件的读取流的时候必须要指定System.IO.FileShare参数为ReadWrite,否则对正在被写入或未被释放的文件的访问将被拒绝,因为写入的进程已经获得了写入权限,作为后来的读取者一定要允许其它进程可以对文件读写,要不然冲突就是一定的了。

3.关于日志的清理

随着程序常年运行,日志积累得越来越多,而日志应该都有一定的时效性,过了时效期后的日志就没有什么价值了,所以应该对日志做定时的清理操作,因此写日志的时候应该有一个默认的时效值,使日志在到期之后自动删除,以免无限增多浪费了磁盘空间,毕竟磁盘空间是十分有限的。

下面开始上代码:

新建一个 .Net Standard 类库,命名 Logger ,在类库中添加一个 Core 文件夹,在 Core 文件夹添加以下文件:

  1. ILog.cs 接口
  2. Log.cs 密封的接口实现类(不对程序集外提供访问)
  3. TextFileReader.cs 文本文件读取
  4. Factory.cs 工厂类(生产和维护日志对象)
 namespace Logger.Core
{
public interface ILog
{
void Write(string logInfo);
void WriteFormat(string format, params object[] args);
void SaveLogToFile();
void ClearLogFile();
}
}

ILog.cs

 namespace Logger.Core
{
internal class Log : ILog
{
private System.Text.StringBuilder logSource = null;
private string logFilePre = string.Empty;
private System.IO.FileStream fileStream = null;
private DateTime logFileScanLastTime = DateTime.Now;
private int logFileRetentionDays = ; public Log(string logFilePre)
: this(logFilePre, )
{ }
public Log(string logFilePre, int logFileRetentionDays)
{
this.logFilePre = logFilePre;
this.logSource = new System.Text.StringBuilder();
if (logFileRetentionDays < )
{
logFileRetentionDays = ;
}
this.logFileRetentionDays = logFileRetentionDays;
Factory.SetFileThreadStart();
} private System.IO.FileStream GetFileStream()
{
if (!System.IO.Directory.Exists(Factory.logsDirPath))
{
System.IO.Directory.CreateDirectory(Factory.logsDirPath);
}
System.IO.FileStream fs;
string FilePath = System.IO.Path.Combine(Factory.logsDirPath, this.logFilePre + DateTime.Now.ToString("yyyyMMdd") + ".log");
if (!System.IO.File.Exists(FilePath))
{
if (fileStream != null)
{
fileStream.Close();
}
fileStream = fs = new System.IO.FileStream(FilePath, System.IO.FileMode.CreateNew, System.IO.FileAccess.Write, System.IO.FileShare.Read, , true);
}
else
{
if (fileStream != null)
{
fs = fileStream;
}
else
{
fileStream = fs = new System.IO.FileStream(FilePath, System.IO.FileMode.Open, System.IO.FileAccess.Write, System.IO.FileShare.Read, , true);
}
}
return fs;
}
private string GetLogText()
{
string s = "";
if (logSource.Length > )
{
lock (logSource)
{
s = logSource.ToString();
logSource.Clear();
}
}
return s;
} public void Write(string logInfo)
{
try
{
if (logSource == null)
{
logSource = new System.Text.StringBuilder();
}
lock (this)
{
logSource.AppendFormat("{0} {1}{2}", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss,fff"), logInfo, System.Environment.NewLine);
}
}
catch { }
}
public void WriteFormat(string format, params object[] args)
{
try
{
if (logSource == null)
{
logSource = new System.Text.StringBuilder();
}
lock (this)
{
logSource.AppendFormat("{0} ", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss,fff"));
logSource.AppendFormat(format, args);
logSource.Append(System.Environment.NewLine);
}
}
catch { }
}
public void SaveLogToFile()
{
try
{
string logInfo = GetLogText();
if (logInfo.Length > )
{
System.IO.FileStream fs = GetFileStream();
byte[] buffer = System.Text.UTF8Encoding.UTF8.GetBytes(logInfo);
long lockBegin = fs.Length;
long lockEnd = buffer.Length;
fs.Position = lockBegin;
fs.Lock(lockBegin, lockEnd);
//fs.WriteAsync(buffer, 0, buffer.Length);
fs.Write(buffer, , buffer.Length);
fs.Unlock(lockBegin, lockEnd);
fs.Flush();
//fs.Close();
}
}
catch { }
}
public void ClearLogFile()
{
if ((DateTime.Now - logFileScanLastTime).TotalMinutes < )
{
return;
}
logFileScanLastTime = DateTime.Now;
System.IO.DirectoryInfo directoryInfo = new System.IO.DirectoryInfo(Factory.logsDirPath);
if (!directoryInfo.Exists)
{
return;
}
System.IO.FileInfo[] files = directoryInfo.GetFiles(this.logFilePre + "*.log", System.IO.SearchOption.TopDirectoryOnly);
if (files == null || files.Length < )
{
return;
}
DateTime time = DateTime.Now.AddDays( - logFileRetentionDays);
foreach (System.IO.FileInfo file in files)
{
try
{
if (file.CreationTime < time)
{
file.Delete();
}
}
catch { }
}
} }
}

Log.cs

 namespace Logger.Core
{
public class TextFileReader
{
bool _readStart = false;
bool _readEnd = false;
System.IO.FileStream _stream = null;
System.Text.Encoding _code = null;
long _fileLength = ;
long _currentPosition = ;
string _readStr = string.Empty;
int _readBytes = ;
string _filePath = "";
readonly string[] _defaultOffsetStrArray = new string[] { System.Environment.NewLine, " ", ",", ".", "!", "?", ";", ",", "。", "!", "?", ";" };
string[] _offsetStrArray = null; public string ReadStr {
get { return _readStr; }
}
public string FilePath {
get { return _filePath; }
set { _filePath = value; }
}
public int ReadBytes {
get { return _readBytes < ? : _readBytes; }
set { _readBytes = value; }
}
public string[] OffsetStrArray {
get { return (_offsetStrArray == null|| _offsetStrArray.Length < )? _defaultOffsetStrArray : _offsetStrArray; }
set { _offsetStrArray = value; }
} public TextFileReader() {
_offsetStrArray = _defaultOffsetStrArray;
}
public TextFileReader(string FilePath)
{
this.FilePath = FilePath;
_offsetStrArray = _defaultOffsetStrArray;
}
private int GetPosition(string readStr, string[] offsetStrArray)
{
int position = -;
for (int i = ; i < offsetStrArray.Length; i++)
{
position = readStr.LastIndexOf(offsetStrArray[i]);
if (position > )
{
break;
}
}
return position;
}
public bool Read()
{
if (!_readStart)
{
//System.IO.FileShare.ReadWrite:允许其它程序读写文件(重要,否则很可能会与负责写入的程序冲突而被拒绝访问)
_stream = new System.IO.FileStream(this.FilePath, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite);
_code = GetType(this.FilePath);
_currentPosition = ;
_fileLength = _stream.Length;
_readStart = true;
}
if (_currentPosition < _fileLength)
{
byte[] readBuffer = new byte[this.ReadBytes];
//设置读取位置
_stream.Seek(_currentPosition, System.IO.SeekOrigin.Begin);
//本次实际读到的字节数
int currentReadBytes = _stream.Read(readBuffer, , readBuffer.Length);
//读取位置偏移
_currentPosition += currentReadBytes; //实际读到的字节少于指定的字节数(在读到最后一批时)
if (currentReadBytes < _readBytes)
{
byte[] temp = new byte[currentReadBytes];
int index = ;
while (index < currentReadBytes)
{
temp[index] = readBuffer[index];
index++;
}
readBuffer = temp;
}
_readStr = _code.GetString(readBuffer);
//如果没有读到最后一个字节则计算位置偏移
if (_currentPosition < _fileLength)
{
int offsetStrPosition = GetPosition(_readStr, this.OffsetStrArray);
if (offsetStrPosition > )//找到内容则计算位置偏移
{
//提取将被移除的内容
string removeStr = _readStr.Substring(offsetStrPosition + );
//移除内容
_readStr = _readStr.Remove(offsetStrPosition + );
//位置后退
_currentPosition = _currentPosition - _code.GetBytes(removeStr).Length;
}
}
}
else
{
_readEnd = true;
_stream.Dispose();
}
return !_readEnd;
} public static System.Text.Encoding GetType(string fullname)
{
System.IO.FileStream fs = new System.IO.FileStream(fullname, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite);
System.Text.Encoding r = GetType(fs);
fs.Close();
return r;
}
public static System.Text.Encoding GetType(System.IO.FileStream fs)
{
byte[] Unicode = new byte[] { 0xFF, 0xFE, 0x41 };
byte[] UnicodeBIG = new byte[] { 0xFE, 0xFF, 0x00 };
byte[] UTF8 = new byte[] { 0xEF, 0xBB, 0xBF };
System.Text.Encoding reVal = System.Text.Encoding.Default; System.IO.BinaryReader r = new System.IO.BinaryReader(fs, System.Text.Encoding.Default);
int i;
int.TryParse(fs.Length.ToString(), out i);
byte[] ss = r.ReadBytes(i);
if (IsUTF8Bytes(ss) || (ss[] == 0xEF && ss[] == 0xBB && ss[] == 0xBF))
{
reVal = System.Text.Encoding.UTF8;
}
else if (ss[] == 0xFE && ss[] == 0xFF && ss[] == 0x00)
{
reVal = System.Text.Encoding.BigEndianUnicode;
}
else if (ss[] == 0xFF && ss[] == 0xFE && ss[] == 0x41)
{
reVal = System.Text.Encoding.Unicode;
}
r.Close();
return reVal;
}
private static bool IsUTF8Bytes(byte[] data)
{
int charByteCounter = ;
byte curByte;
for (int i = ; i < data.Length; i++)
{
curByte = data[i];
if (charByteCounter == )
{
if (curByte >= 0x80)
{
while (((curByte <<= ) & 0x80) != )
{
charByteCounter++;
}
if (charByteCounter == || charByteCounter > )
{
return false;
}
}
}
else
{
if ((curByte & 0xC0) != 0x80)
{
return false;
}
charByteCounter--;
}
}
if (charByteCounter > )
{
throw new Exception("非预期的byte格式");
}
return true;
} }
}

TextFileReader.cs

 namespace Logger.Core
{
public static class Factory
{
private static object setFileThreadCreateLockObj = new object();
private static object loggerCreateLockObj = new object();
public static readonly string logsDirPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs");
internal static readonly System.Collections.Generic.Dictionary<string, ILog> loggerDic = new System.Collections.Generic.Dictionary<string, ILog>();
internal static System.Threading.Thread setFileThread = null;
internal static void SetFileThreadStartFunc(object obj)
{
while (true)
{
try
{
foreach (string key in loggerDic.Keys)
{
loggerDic[key].SaveLogToFile();
loggerDic[key].ClearLogFile();
}
System.Threading.Thread.Sleep();
}
catch { }
}
}
public static void SetFileThreadStart()
{
if (setFileThread == null)
{
lock (setFileThreadCreateLockObj)
{
if (setFileThread == null)
{
setFileThread = new System.Threading.Thread(new System.Threading.ParameterizedThreadStart(SetFileThreadStartFunc));
setFileThread.IsBackground = true;
setFileThread.Start(null);
}
}
}
}
public static ILog GetLogger()
{
return GetLogger("Trace");
}
public static ILog GetLogger(string LogFilePre)
{
return GetLogger(LogFilePre, );
}
public static ILog GetLogger(string logFilePre, int logFileRetentionDays)
{
logFilePre = GetLogFilePre(logFilePre);
if (loggerDic.ContainsKey(logFilePre))
{
return loggerDic[logFilePre];
}
else
{
lock (loggerCreateLockObj)
{
if (loggerDic.ContainsKey(logFilePre))
{
return loggerDic[logFilePre];
}
else
{
ILog _logger = new Log(logFilePre, logFileRetentionDays);
loggerDic.Add(logFilePre, _logger);
return _logger;
}
}
}
}
public static string GetLogFilePre(string logFilePre)
{
if (string.IsNullOrEmpty(logFilePre)) {
logFilePre = "Trace";
}
logFilePre = logFilePre.ToLower();
if (!logFilePre.EndsWith("-"))
{
logFilePre = logFilePre + "-";
}
logFilePre = logFilePre.Substring(, ).ToUpper() + logFilePre.Substring();
return logFilePre;
}
public static System.Collections.Generic.List<string> GetLogFilePreList()
{
System.Collections.Generic.List<string> reval = new System.Collections.Generic.List<string>();
foreach(string key in loggerDic.Keys)
{
reval.Add(key);
}
return reval;
} }
}

Factory.cs

以上是实现日志功能的核心代码,下面在类库项目下直接添加两个静态类:

  1. LogWriter.cs 负责写,定义了常规的 Fatal , Error , Info , Debug 等方法及默认的日志时效期
  2. LogReader.cs 负责读,如获取日志类型列表,获取日志文件列表,或取日志文件的TextFileReader对象等
 namespace Logger
{
public static class LogWriter
{
public static Core.ILog Debug()
{
return Core.Factory.GetLogger("Debug", );
}
public static Core.ILog Debug(string logInfo)
{
Core.ILog logger = Debug();
logger.Write(logInfo);
return logger;
}
public static Core.ILog Debug(string format, params object[] args)
{
Core.ILog logger = Debug();
logger.WriteFormat(format, args);
return logger;
}
public static Core.ILog Info()
{
return Core.Factory.GetLogger("Info", );
}
public static Core.ILog Info(string logInfo)
{
Core.ILog logger = Info();
logger.Write(logInfo);
return logger;
}
public static Core.ILog Info(string format, params object[] args)
{
Core.ILog logger = Info();
logger.WriteFormat(format, args);
return logger;
}
public static Core.ILog Error()
{
return Core.Factory.GetLogger("Error", );
}
public static Core.ILog Error(string logInfo)
{
Core.ILog logger = Error();
logger.Write(logInfo);
return logger;
}
public static Core.ILog Error(string format, params object[] args)
{
Core.ILog logger = Error();
logger.WriteFormat(format, args);
return logger;
}
public static Core.ILog Fatal()
{
return Core.Factory.GetLogger("Fatal", );
}
public static Core.ILog Fatal(string logInfo)
{
Core.ILog logger = Fatal();
logger.Write(logInfo);
return logger;
}
public static Core.ILog Fatal(string format, params object[] args)
{
Core.ILog logger = Fatal();
logger.WriteFormat(format, args);
return logger;
}
}
}

LogWriter.cs

 namespace Logger
{
public static class LogReader
{
public static System.Collections.Generic.List<string> GetLogFilePreList()
{
return Core.Factory.GetLogFilePreList();
}
public static System.IO.FileInfo[] GetLogFiles(string logFilePre)
{
System.IO.DirectoryInfo dir = new System.IO.DirectoryInfo(Core.Factory.logsDirPath);
if (!dir.Exists)
{
return new System.IO.FileInfo[] { };
}
logFilePre = Core.Factory.GetLogFilePre(logFilePre);
System.IO.FileInfo[] fis = dir.GetFiles(logFilePre + "*.log", System.IO.SearchOption.TopDirectoryOnly);
if (fis == null)
{
fis = new System.IO.FileInfo[] { };
}
return fis;
}
public static Core.TextFileReader GetTextFileReader(System.IO.FileInfo logFileInfo)
{
Core.TextFileReader textFileReader = new Core.TextFileReader(logFileInfo.FullName);
textFileReader.ReadBytes = * * ;
return textFileReader;
}
}
}

LogReader

新建一个控制台程序来测试一下,测试代码:

 class Program
{
static void Main(string[] args)
{
Writer();
Reader();
}
static void Writer()
{
for (var i = ; i < ; i++)
{
System.Threading.Thread thread = new System.Threading.Thread(new System.Threading.ParameterizedThreadStart(WriterFunc));
thread.IsBackground = true;
thread.Start(i);
}
}
static void WriterFunc(object num)
{
int threadNum = (int)num;
while (true)
{
Logger.LogWriter.Info("这是线程{0}", threadNum);
Logger.LogWriter.Error("这是线程{0}", threadNum);
Logger.LogWriter.Fatal("这是线程{0}", threadNum);
System.Threading.Thread.Sleep();
}
}
static void Reader()
{
string cmd = "";
while (cmd != "r")
{
Console.Write("输入 r 读取日志:");
cmd = Console.ReadLine();
} System.Collections.Generic.List<string> preList = Logger.LogReader.GetLogFilePreList();
if (preList.Count < )
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("未能检索到日志记录!");
Console.ResetColor();
Reader();
}
Console.WriteLine("-----------------------------------------------------------"); Console.WriteLine("编号\t类型前缀");
Console.ForegroundColor = ConsoleColor.Red;
for (var i = ; i < preList.Count; i++)
{
Console.WriteLine("{0}\t{1}", i + , preList[i]+"*");
}
Console.ResetColor();
Console.WriteLine("-----------------------------------------------------------"); Console.Write("输入编号读取日志文件列表:");
int preNum = GetInputNum(, preList.Count); var files = Logger.LogReader.GetLogFiles(preList[preNum-]);
if (files.Length < )
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("未能检索到日志文件!");
Console.ResetColor();
Reader();
}
Console.WriteLine("-----------------------------------------------------------"); Console.WriteLine("编号\t日志文件");
Console.ForegroundColor = ConsoleColor.Red;
for (var i = ; i < files.Length; i++)
{
Console.WriteLine("{0}\t{1}", i + , System.IO.Path.GetFileName(files[i].FullName));
}
Console.ResetColor();
Console.WriteLine("-----------------------------------------------------------"); Console.Write("输入编号读取日志:");
int fileNum = GetInputNum(, files.Length);
Console.WriteLine("-----------------------------------------------------------"); var reader = Logger.LogReader.GetTextFileReader(files[fileNum - ]);
while (reader.Read())
{
Console.Write(reader.ReadStr);
} Console.WriteLine(); Reader(); }
static int GetInputNum(int min, int max)
{
int num = -;
while (true)
{
string inputNum = Console.ReadLine();
bool flag = false;
if (System.Text.RegularExpressions.Regex.IsMatch(inputNum, @"^\d{1,9}$"))
{
num = Convert.ToInt32(inputNum);
flag = num <= max && num >= min;
}
if (!flag)
{
Console.Write("输入不合法,请重新输入:");
num = -;
}
else
{
break;
}
}
return num;
}
}

Program.cs

程序运行截图:

通过编写一个简单的日志类库来加深了解C#的文件访问控制

至此,一个日志类库就算完成了。

鉴于个人水平问题,不敢妄言更高效或更优雅,但是可以集成到其它项目中工作了,该代码作者在公司的实际项目中有使用。

不用各种繁杂的配置,想写就写,如果想要添加一个其它类型的日志只要在LogWriter.cs中增加方法即可。

(^_^)大神莫笑,小菜莫怕,欢迎善意的沟通和交流!

上一篇:sql 语句 嵌套子查询 执行顺序分析


下一篇:循序渐进学.Net Core Web Api开发系列【10】:使用日志