环境
win10 vs2019 .net core3.1 Nlog4.75
目标
程序记录日志功能,具体要求如下:
记录日志到3个目录,SrvLog(win服务) DBLog(数据库) AppLog(程序),
每年一个目录,名字是年(如:2020),每月一个目录(如:10)
日志文件每达到2M,另起新文件.日志以年与日为名字,例如: 2020-10-04.log
logsroot - >
srvlog -> 2020-10-04.log
dblog -> 2020-10-04.log
applog -> 2020-10-04.log
实现
使用c#的文件操作类可以容易的实现这个要求,缺点是性能和功能都较弱,在要求不高的小型系统下使用没有问题
1 public class LogHelp 2 { 3 /// <summary> 4 /// 日志根目录,默认为当前程序运行目录.要在程序部署前设定. 5 /// 默认是程序运行目录.(设定新的必须是绝对路径)(例 e:/logs 或者 /home/log) 6 /// </summary> 7 public static string LogRootPath = AppDomain.CurrentDomain.BaseDirectory; 8 9 private static ReaderWriterLockSlim LogWriteLock 10 = new ReaderWriterLockSlim(); 11 /// <summary> 12 /// 日志开关设置 off=不记录 nodebug=DeBugLog()这个记录调试日志的方法不记录 其它值=记录 13 /// </summary> 14 private static string logOnOff = "on"; 15 16 /// <summary> 17 /// 用于记录数据库操作(出错时)日志.包含SQL语句和参数,及异常提示信息 18 /// 该日志会位于根目录下的DBLogs文件夹下.且以当天日期为文件名 19 /// </summary> 20 /// <param name="msg"></param> 21 /// <returns></returns> 22 public static void DBLog(string msg, bool yearDir = false, bool monthDir = false, bool dayDir = false) 23 { 24 if (logOnOff == "off") return; 25 Log(msg, "", "DBLog", yearDir, monthDir, dayDir); 26 } 27 /// <summary> 28 /// 添加日志 主要针对WIN服务程序,文件位于根目录的SrvLogs目录下 29 /// </summary> 30 /// <param name="msg"></param> 31 /// <returns></returns> 32 public static void SrvLog(string msg, bool yearDir = false, bool monthDir = false, bool dayDir = false) 33 { 34 if (logOnOff == "off") return; 35 Log(msg, "", "SrvLog", yearDir, monthDir, dayDir); 36 } 37 /// <summary> 38 /// 添加调试日志 主要是未上线时用 39 /// </summary> 40 /// <param name="msg"></param> 41 /// <returns></returns> 42 public static void DeBugLog(string msg, bool yearDir = false, bool monthDir = false, bool dayDir = false) 43 { 44 if (logOnOff == "nodebug") return; 45 if (logOnOff == "off") return; 46 Log(msg, "", "DeBugLog", yearDir, monthDir, dayDir); 47 } 48 /// <summary> 49 /// 添加日志 如果不指定目录名,则文件位于根目录的AppLog默认目录下.日志扩展名固定为.log 50 /// </summary> 51 /// <param name="message">日志内容</param> 52 /// <param name="filename">日志文件名,不含扩展名.省略时以当天年月日为名</param> 53 /// <param name="directory">一类型日志总目录(应用程序根目录的下一级)</param> 54 /// <returns></returns> 55 public static void Log(string message, string filename = "", string logDirName = "AppLog", bool yearDir = false, bool monthDir = false, bool dayDir = false) 56 { 57 // 进入写锁定.如果其它线程也来访问,则等待 58 // 其后至解除锁定之间的代码不能有异常,否则无法解除写锁定 59 LogWriteLock.EnterWriteLock(); 60 // 日志目录与文件名 61 string directory = GetLogPath(logDirName, yearDir, monthDir, dayDir); 62 string fn = filename == "" 63 ? DateTime.Now.Date.ToString("yyyyMMdd") : filename; 64 string path = Path.Combine(directory, fn + ".log"); 65 // 超过2M时存为旧文件,名字如:yyyyMMdd(1) 66 if (File.Exists(path)) 67 { 68 FileInfo fi = new FileInfo(path); 69 if (fi.Length > 2000 * 1000) 70 { 71 int count = 1; 72 while (true) 73 { 74 string oldpath = Path.Combine(directory, $"{fn}({count}).txt"); 75 if (!File.Exists(oldpath)) 76 { 77 fi.MoveTo(oldpath); 78 break; 79 } 80 count++; 81 } 82 } 83 } 84 // 开始写入 85 using (StreamWriter sw = new StreamWriter(path, true)) 86 { 87 // 日志记录时间.指下面获取的当时时间.不应理解为日志记录的时间. 88 // (考虑到并发时,日志缓存到了队列,或者本方法正被访问, 89 // 其它线程正在等读写锁解除 90 string WriteTime = 91 DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss.fff"); 92 // 当前调用该日志方法的线程ID 93 string ThreadId = 94 Thread.CurrentThread.ManagedThreadId.ToString(); 95 string method = System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.FullName; 96 // 97 sw.WriteLine( 98 $"WriteTime:{WriteTime} TId-{ThreadId}] [{method}{Environment.NewLine}{message}{Environment.NewLine}"); 99 } 100 // 解除写锁定 101 LogWriteLock.ExitWriteLock(); 102 // 103 } 104 105 /// <summary> 106 /// 获取日志根目录 如 e:/logs/ 如果目录不存在,则会建立 107 /// 注意:未加异常判断.请保证根目录设置(可能在webconfig的rootPath)及目录名有效 108 /// </summary> 109 /// <param name="logDirName">日志根目录的名字</param> 110 /// <param name="day">是否以天建立目录</param> 111 /// <param name="month">是否以月建立目录</param> 112 /// <param name="year">是否以年建立目录</param> 113 /// <returns></returns> 114 private static string GetLogPath(string logDirName = "", bool yearDir = false, bool monthDir = false, bool dayDir = false) 115 { 116 string logPath = logDirName == "" ? "Logs" : logDirName; 117 string rootpath = string.IsNullOrWhiteSpace(LogRootPath) ? AppDomain.CurrentDomain.BaseDirectory : LogRootPath; 118 string directory = string.Format(@"{0}/{1}/", rootpath, logPath); 119 if (yearDir == true) 120 directory = string.Format(@"{0}{1}y/", directory, DateTime.Today.Year); 121 if (monthDir == true) 122 directory = string.Format(@"{0}{1}m/", directory, DateTime.Today.Month); 123 if (dayDir == true) 124 directory = string.Format(@"{0}{1}d/", directory, DateTime.Today.Day); 125 // 126 if (!Directory.Exists(directory)) 127 { 128 Directory.CreateDirectory(directory); 129 } 130 // 131 return directory; 132 } 133 }
log4net
这个以前用过了,但是不想再用了,因为过于复杂的配置太麻烦了,这次又想起它来,针对目标研究了一下:
要实现日志按年月建立目录,关键在于这几个地方:
选择日志输出为文件回转: RollingFileAppender,这个就是所谓文件回转生成日志,就是日志到大小了,就新建文件
staticLogFileName // 这个必须为false
file = "logs/" // 这里写目录名字,而不是文件名字,后面有个 /
datepattern = "yyyy/MM/yyyy-MM-dd'.log'" // 这个设定就是生成年月目录的关键,除了单引号里的 '.log',其它的回解释成日期对于的部分
文件大小,保留文件数目,设置 2*1024*1024 (2M) 100(最多备份100个文件)
还有个文件名扩展名保留的选项,设为true,不然备份的日志就每又扩展名.
这样设置后,测试发现一个问题,在程序打印日志后,确实按预期实现了,生成了日志:
logs - > 2020 -> 10 -> 2020-10-04.log
-> 2020-10-04.1.log
-> 2020-10-04.2.log
但是
再次运行程序,发现日志 2020-10-04.log 被追加了,这个正常,但是到大小后,却没有生成 2020-10-04.3.log ,而是覆盖了 2020-10-04.1.log
预期应该是这样的:
logs - > 2020 -> 10 -> 2020-10-04.log
-> 2020-10-04.1.log
-> 2020-10-04.2.log
-> 2020-10-04.3.log
这个问题搞了很久,没有解决,然后,就放弃了!!
哎,,曾经惧怕的log4net配置,这一次发现,其实没有什么的,不喜欢xml的配置,用代码也可以的,还比较简便.XML是太啰嗦了!
NLog
文档 https://nlog-project.org/config/?tab=targets
无奈之下放弃了log4net,同时发现了NLog
这个日志的设计和log4net很相似啊,开始有一种恐惧感.
不过很快发现,这个不错,是C#开发的."正宗日志库".
最后使用这个实现了目标
它的配置类似于log4net,但是,却没有发现log4net的这个情况.真是救星啊!.
要实现这3个目录的日志记录器,需要实现3个file目标(就是3个输出到文件的配置),
配置项目就只有目录不一样,其它的一样,
这时,把记录器的名字设置成目录的名字,然后再日志路径属性上设置 ${logger},例如
@"${basedir}/${logger}/${date:format=yyyy}/${date:format=MM}/${shortdate}.log"
${basedir} 根目录
${logger} 记录器的名字 这里就是 SrvLog DBLog AppLog
${date:format=yyyy} 每年一个目录
${shortdate}.log 当天时间为名字 2020-10-04.log
采坑记录:
为了实现分3个目录,开始时使用字符串替换的方式,修改路径中 ${logger} 这一段目录的值,
这样做的结果发现,3个日志记录器的所有日志内容,都写到这3个目录的文件里了:
下面3个记录器,期望是各写各的目录,结果所有内容都写到一起了,每份日志都有这3个记录器的内容.
这简直就是灾难啊!.然而,偶然发现: 上面的方法后: 在路径中使用变量 ${logger},就不会这样了..
logSrv.Info(msg);
logDB.Info(msg);
log.Info(msg);
public static class NLogHelp { /// <summary> /// 日志根目录,默认为当前程序运行目录.要在程序部署前设定. /// 默认是程序运行目录.(设定新的必须是绝对路径)(例 e:/logs 或者 /home/log) /// </summary> private static readonly string LogRootPath; // 3种目录的记录器 // 1.输出到文件(用于数据库,文件夹 DBLog) private static readonly Logger logDB; // 2.输出到文件(用于服务,文件夹 SrvLog) private static readonly Logger logSrv; // 3.输出到文件(用于App,Web,文件夹 AppLog) private static readonly Logger log; // 日志格式说明 // ${date} 日期,例: 2020/10/03 12:10:01.749 // ${threadid} 线程ID,例: TID-1 // ${callsite}/${callsite-linenumber} 类方法行号,例: LibTest.Program.Main/102 // ${newline} 换行,跨平台的 // ${message}日志内容 private static readonly string contLayout = @"${date} TID-${threadid} ${callsite}/${callsite-linenumber}${newline}${message}${newline}${newline}"; // 日志目录文件名,模式 // ${basedir} 当前应用程序运行根目录.可以修改LogRootPath属性(必须在程序每次部署前) // ${data:format=yyyy}每年一个目录 // ${data:format=MM}每月一个目录 // 如果日志多,可以继续分下级目录. // ${shortdate}.log 以每天年月日做文件名字,2020-10-04.log private static readonly string fileNameTpl = @"${basedir}/${logger}/${date:format=yyyy}/${date:format=MM}/${shortdate}.log"; // 日志大小2M上限后,新开文件 private static readonly long maxFileSize = 2 * 1024 * 1024; static NLogHelp() { // 1.根目录检查 string fileFullPath = fileNameTpl; if (!string.IsNullOrWhiteSpace(LogRootPath)) { fileFullPath = fileNameTpl.Replace("${basedir}", LogRootPath); } // 2.初始化3个文件型日志记录器,区别只在于目录设置不同 var cfg = new LoggingConfiguration(); string[] logType = { "SrvLog", "DBLog", "AppLog" }; for (int i = 0; i < 3; i++) { var target = new FileTarget { Name = logType[i], Layout = contLayout, FileName = fileFullPath, ArchiveAboveSize = maxFileSize, // 这个要开启,否则性能极差 // https://github.com/NLog/NLog/wiki/File-target KeepFileOpen = true, ConcurrentWrites = false }; // 加入到配置器 cfg.AddTarget(target); // 级别(Trace,Debug,Info,Warn,Error,Fatal),这里都用Info cfg.AddRule(LogLevel.Info, LogLevel.Info, target); } // 3.加载配置,生成 LogManager.Configuration = cfg; // 绑定变量 logSrv = LogManager.GetLogger(logType[0]); logDB = LogManager.GetLogger(logType[1]); log = LogManager.GetLogger(logType[2]); } public static void SrvLog(string msg) { logSrv.Info(msg); } public static void DBLog(string msg) { logDB.Info(msg); } public static void Log(string msg) { log.Info(msg); } }