使用 StreamWriter 实现滚动日志,并从顶部删除

Posted

技术标签:

【中文标题】使用 StreamWriter 实现滚动日志,并从顶部删除【英文标题】:Using StreamWriter to implement a rolling log, and deleting from top 【发布时间】:2013-06-01 17:37:20 【问题描述】:

我的 C# winforms 4.0 应用程序一直在使用线程安全的流编写器来执行内部调试日志信息。当我的应用程序打开时,它会删除该文件,然后重新创建它。当应用关闭时,它会保存文件。

我想做的是修改我的应用程序,以便它执行追加而不是替换。这是一个简单的修复。

但是,这是我的问题:

我希望将我的日志文件保持在最大 10 兆字节左右。我的约束很简单。当你去关闭文件时,如果文件大于10兆,把前10%剪掉。

有没有比以下“更好”的方法:

    关闭文件 检查文件是否大于 10 兆 如果是,请打开文件 解析整个事情 剔除前 10% 写回文件 关闭

编辑:好吧,我最终推出了自己的建议(如下所示)将公开迁移到 Log4Net 的建议是一个很好的建议,但是学习新库并移动我所有的日志语句(数千条)需要花费的时间对于我尝试进行的小幅改进来说,时间不够用。

  private static void PerformFileTrim(string filename)
  
     var FileSize = Convert.ToDecimal((new System.IO.FileInfo(filename)).Length);

     if (FileSize > 5000000)
     
        var file = File.ReadAllLines(filename).ToList();
        var AmountToCull = (int)(file.Count * 0.33); 
        var trimmed = file.Skip(AmountToCull).ToList();
        File.WriteAllLines(filename, trimmed);
     
  

【问题讨论】:

那么使用为日志编写的特定库已经实现了这个功能呢? (想到Log4Net) 我没有花时间研究 Log4Net。到目前为止,我的简单流写器类做得很好。如果 Log4Net 允许我正在寻找的“滚动”日志,那么它绝对是我应该使用的解决方案。 未来读者:"...[to] move all my log statements (thousands) over isn't time effective" - 这就是我们依赖依赖注入和接口的原因。 :) 一个更好的设计,比如将ILogger (例如)实例注入到所有需要日志记录的类 - 或者甚至只有一个主日志记录类/函数 - 可以让您在日志记录要求发生变化时在一个位置修改所有日志记录功能. 【参考方案1】:

我正在查看 win32 api,但我什至不确定是否可以使用本机 win32 vfs 调用来执行此操作,更不用说通过 .Net。

关于我唯一的解决方案是使用内存映射文件并手动移动数据,.Net 似乎从 .Net 4.0 开始支持。

Memory Mapped Files

【讨论】:

内存映射文件对于 10mb 文件来说太过分了【参考方案2】:

我喜欢 greggorob64 的解决方案,但也想压缩旧文件。除了将旧文件压缩为 zip 的部分之外,这包含您需要的一切,您可以在此处找到:Create zip file in memory from bytes (text with arbitrary encoding)

static int iMaxLogLength = 2000; // Probably should be bigger, say 200,000
static int KeepLines = 5; // minimum of how much of the old log to leave

public static void ManageLogs(string strFileName)

    try
    
        FileInfo fi = new FileInfo(strFileName);
        if (fi.Length > iMaxLogLength) // if the log file length is already too long
        
            int TotalLines = 0;
                var file = File.ReadAllLines(strFileName);
                var LineArray = file.ToList();
                var AmountToCull = (int)(LineArray.Count - KeepLines);
                var trimmed = LineArray.Skip(AmountToCull).ToList();
                File.WriteAllLines(strFileName, trimmed);
                string archiveName = strFileName + "-" + DateTime.Now.ToString("MM-dd-yyyy") + ".zip";
                File.WriteAllBytes(archiveName, Compression.Zip(string.Join("\n", file)));
        

    
    catch (Exception ex)
    
        Console.WriteLine("Failed to write to logfile : " + ex.Message);
    

我将此作为我的应用程序的初始化/重新初始化部分的一部分,因此它每天运行几次。

ErrorLogging.ManageLogs("Application.log");

【讨论】:

【参考方案3】:

此功能将允许您根据工作日轮换日志。第一次我们的应用程序将在星期一启动,将检查星期一日期的任何现有条目,如果今天尚未初始化,将丢弃旧条目并重新初始化新文件。从那天开始,file 将继续将文本附加到同一个日志文件中。

因此,总共将创建 7 个日志文件。 debug-Mon.txt、debog-Tue.txt...

它还将添加实际记录消息的方法名称以及日期时间。对于一般用途非常有用。

private void log(string text)
        
            string dd = DateTime.Now.ToString("yyyy-MM-dd");
            string mm = DateTime.Now.ToString("ddd");

            if (File.Exists("debug-" + mm + ".txt"))
            
                String contents = File.ReadAllText("debug-" + mm + ".txt");


                if (!contents.Contains("Date: " + dd))
                
                    File.Delete("debug-" + mm + ".txt");
                
            

            File.AppendAllText("debug-" + mm + ".txt", "\r\nDate: " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:s") + " =>\t" + new System.Diagnostics.StackFrame(1, true).GetMethod().Name + "\t" + text);

        

【讨论】:

【参考方案4】:

这里的解决方案并不适合我。我拿了 user3902302 的答案,这又是基于 bigtech 的答案,并写了一个完整的课程。另外,我没有使用 StreamWriter,您可以更改一行(AppendAllText 与 StreamWrite 等效项)。

几乎没有错误处理(例如,当它失败时重试访问,尽管锁应该捕获所有内部并发访问)。

对于一些以前不得不使用像 log4net 或 nlog 这样的大型解决方案的人来说,这可能就足够了。 (而且 log4net RollingAppender 甚至不是线程安全的,这个是。:))

public class RollingLogger

    readonly static string LOG_FILE = @"c:\temp\logfile.log";
    readonly static int MaxRolledLogCount = 3;
    readonly static int MaxLogSize = 1024; // 1 * 1024 * 1024; <- small value for testing that it works, you can try yourself, and then use a reasonable size, like 1M-10M

    public static void LogMessage(string msg)
    
        lock (LOG_FILE) // lock is optional, but.. should this ever be called by multiple threads, it is safer
        
            RollLogFile(LOG_FILE);
            File.AppendAllText(LOG_FILE, msg + Environment.NewLine, Encoding.UTF8);
        
    

    private static void RollLogFile(string logFilePath)
    
        try
        
            var length = new FileInfo(logFilePath).Length;

            if (length > MaxLogSize)
            
                var path = Path.GetDirectoryName(logFilePath);
                var wildLogName = Path.GetFileNameWithoutExtension(logFilePath) + "*" + Path.GetExtension(logFilePath);
                var bareLogFilePath = Path.Combine(path, Path.GetFileNameWithoutExtension(logFilePath));
                string[] logFileList = Directory.GetFiles(path, wildLogName, SearchOption.TopDirectoryOnly);
                if (logFileList.Length > 0)
                
                    // only take files like logfilename.log and logfilename.0.log, so there also can be a maximum of 10 additional rolled files (0..9)
                    var rolledLogFileList = logFileList.Where(fileName => fileName.Length == (logFilePath.Length + 2)).ToArray();
                    Array.Sort(rolledLogFileList, 0, rolledLogFileList.Length);
                    if (rolledLogFileList.Length >= MaxRolledLogCount)
                    
                        File.Delete(rolledLogFileList[MaxRolledLogCount - 1]);
                        var list = rolledLogFileList.ToList();
                        list.RemoveAt(MaxRolledLogCount - 1);
                        rolledLogFileList = list.ToArray();
                    
                    // move remaining rolled files
                    for (int i = rolledLogFileList.Length; i > 0; --i)
                        File.Move(rolledLogFileList[i - 1], bareLogFilePath + "." + i + Path.GetExtension(logFilePath));
                    var targetPath = bareLogFilePath + ".0" + Path.GetExtension(logFilePath);
                    // move original file
                    File.Move(logFilePath, targetPath);
                
            
        
        catch (Exception ex)
        
            System.Diagnostics.Debug.WriteLine(ex.ToString());
        
    

编辑: 因为我刚刚注意到你问了一个稍微不同的问题:如果你的线条大小有很大差异,这将是一个变化(虽然在 90% 的情况下不会比你的有所改善,而且可能会稍微快一点,还介绍了一个新的未处理错误(\n 不存在)):

    private static void PerformFileTrim(string filename)
    
        var fileSize = (new System.IO.FileInfo(filename)).Length;

        if (fileSize > 5000000)
        
            var text = File.ReadAllText(filename);
            var amountToCull = (int)(text.Length * 0.33);
            amountToCull = text.IndexOf('\n', amountToCull);
            var trimmedText = text.Substring(amountToCull + 1);
            File.WriteAllText(filename, trimmedText);
        
    

【讨论】:

我喜欢你的解决方案,但为什么readonly static 是常数?【参考方案5】:

这来自bigtech的回答:

private static string RollLogFile()
    
        string path = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
        string appName = Path.GetFileNameWithoutExtension(Environment.GetCommandLineArgs()[0]);
        string wildLogName = string.Format("0*.log",appName);

        int fileCounter = 0;
        string[] logFileList = Directory.GetFiles(path, wildLogName, SearchOption.TopDirectoryOnly);
        if (logFileList.Length > 0)
        
            Array.Sort(logFileList, 0, logFileList.Length);
            fileCounter = logFileList.Length - 1;
            //Make sure we apply the MaxLogCount (but only once to reduce the delay) 
            if (logFileList.Length > MaxLogCount)
            
                //Too many files - remove one and rename the others
                File.Delete(logFileList[0]);
                for (int i = 1; i < logFileList.Length; i++)
                
                    File.Move(logFileList[i], logFileList[i - 1]); 
                
                --fileCounter;
            

            string currFilePath = logFileList[fileCounter];
            FileInfo f = new FileInfo(currFilePath);
            if (f.Length < MaxLogSize)
            
                //still room in the current file
                return currFilePath;
            
            else
            
                //need another filename
                ++fileCounter;                  
            

        
        return string.Format("0123:00.log", path, Path.DirectorySeparatorChar, appName, fileCounter);
    

用法:

string logFileName = RollLogFile();
using (StreamWriter sw = new StreamWriter(logFileName, true))

    sw.AutoFlush = true;
    sw.WriteLine(string.Format("0:u 1", DateTime.Now, message));

【讨论】:

【参考方案6】:

我对此进行了一次研究,但从未提出任何建议,但我可以在这里为您提供 B 计划:

我使用下面的选择来保留最多 3 个日志文件。首先,创建日志文件 1 并将其附加到。当它超过 maxsize 时,会创建 log 2 和更高版本的 log 3。当 log 3 太大时, log 1 被删除,剩余的 log 被下推。

string[] logFileList = Directory.GetFiles(Path.GetTempPath(), "add_all_*.log", SearchOption.TopDirectoryOnly);
if (logFileList.Count() > 1)

    Array.Sort(logFileList, 0, logFileList.Count());


if (logFileList.Any())

    string currFilePath = logFileList.Last();
    string[] dotSplit = currFilePath.Split('.');
    string lastChars = dotSplit[0].Substring(dotSplit[0].Length - 3);
    ctr = Int32.Parse(lastChars);
    FileInfo f = new FileInfo(currFilePath);

    if (f.Length > MaxLogSize)
    
        if (logFileList.Count() > MaxLogCount)
        
            File.Delete(logFileList[0]);
            for (int i = 1; i < MaxLogCount + 1; i++)
            
                Debug.WriteLine(string.Format("moving: 0 1", logFileList[i], logFileList[i - 1]));
                File.Move(logFileList[i], logFileList[i - 1]); // push older log files back, in order to pop new log on top
            
        
        else
        
            ctr++;
        
    

【讨论】:

我必须进行一些修改才能让它为我运行 - 由于尺寸限制,我已将其添加为单独的答案...

以上是关于使用 StreamWriter 实现滚动日志,并从顶部删除的主要内容,如果未能解决你的问题,请参考以下文章

[go]zap配合logrotate实现日志滚动

WKInterfaceTable 添加滚动条“flash”并从非滚动视图中删除滚动条

C# 单元测试 StreamWriter 参数

c#中文件读写StreamWriter问题

在加载时显示特定幻灯片,暂停然后滚动所有幻灯片并从 Timeline.js 开始

log4j 滚动日志 及 实现操作日志