在再次实例化另一个文件后,StreamWriter继续写入同一文件

问题描述

我正在尝试实现一个简单的文件记录器对象,有可能在文件大小达到阈值时将其截断。

我使用的是StreamWriter,在方法Log()的每次调用时都会被写入。为了决定何时截断,我在每次写入之前检查StreamWriter.BaseStream.Length属性,如果它大于阈值,则关闭StreamWriter,创建一个文件并打开{{1 }}。 例如,如果我将阈值设置为10Mb文件,则每写入10Mb数据就会创建一个文件

在正常负载下(假设两次调用StreamWriter间的间隔为3-4秒),一切都会正常进行。但是,将要使用此记录器的产品将处理大量数据,并且需要每1秒记录一次,甚至更少。

问题在于记录器似乎完全忽略了新文件的创建(并打开新流),未能截断该文件并继续写入现有流。

我还尝试手动计算流的长度,希望这是流的问题,但它不起作用。

我发现逐步调试器可以使其正常工作,但不能解决我的问题。每秒记录一次似乎会使程序完全跳过Log()方法

UpdateFile()

用法示例:

public class Logger
{
    private static Logger _logger;
    private const string LogsDirectory = "Logs";

    private StreamWriter _streamWriter;
    private string _path;
    private readonly bool _truncate;
    private readonly int _maxSizeMb;
    private long _currentSize;

    //===========================================================//
    public static void Set(string filename,bool truncate = false,int maxSizeMb = 10)
    {
        if (_logger == null)
        {
            if (filename.Contains('_'))
            {
                throw new Exception("Filename cannot contain the _ character!");
            }

            if (filename.Contains('.'))
            {
                throw new Exception("The filename must not include the extension");
            }
            _logger = new Logger(filename,truncate,maxSizeMb);
        }
    }

    //===========================================================//
    public static void Log(string message,LogType logType = LogType.Info)
    {
        _logger?.InternalLog(message,logType);
    }

    //===========================================================//
    public static void LogException(Exception ex)
    {
        _logger?.InternalLogException(ex);
    }

    //===========================================================//
    private Logger(string filename,int maxSizeMb = 10)
    {
        _path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory,LogsDirectory,$"{filename}_{DateTimetoPrefix(DateTime.Now)}.log");
        if (CheckForExistingLogs())
        {
            _path = GetLatestLogFilename();
        }
        _truncate = truncate;
        _maxSizeMb = maxSizeMb;
        _streamWriter = new StreamWriter(File.Open(_path,FileMode.Append,FileAccess.Write,FileShare.ReadWrite));
        _currentSize = _streamWriter.BaseStream.Length;
    }

    //===========================================================//
    private bool CheckForExistingLogs()
    {
        var directory = Path.GetDirectoryName(_path);
        var filename = Path.GetFileNameWithoutExtension(_path);
        if (filename.Contains('_'))
        {
            filename = filename.Split('_').First();
        }
        return new DirectoryInfo(directory).GetFiles().Any(x => x.Name.ToLower().Contains(filename.ToLower()));
    }

    //===========================================================//
    private string GetLatestLogFilename()
    {
        var directory = Path.GetDirectoryName(_path);
        var filename = Path.GetFileNameWithoutExtension(_path);
        if (filename.Contains('_'))
        {
            filename = filename.Split('_').First();
        }
        var files = new DirectoryInfo(directory).GetFiles().Where(x => x.Name.ToLower().Contains(filename.ToLower()));
        files = files.OrderBy(x => PrefixToDateTime(x.Name.Split('_').Last()));
        return files.Last().FullName;
    }

    //===========================================================//
    private void UpdateFile()
    {
        _streamWriter.Flush();
        _streamWriter.Close();
        _streamWriter.dispose();
        _streamWriter = StreamWriter.Null;
        _path = GenerateNewFilename();
        _streamWriter = new StreamWriter(File.Open(_path,FileShare.ReadWrite));
        _currentSize = _streamWriter.BaseStream.Length;
    }

    //===========================================================//
    private string GenerateNewFilename()
    {
        var directory = Path.GetDirectoryName(_path);
        var oldFilename = Path.GetFileNameWithoutExtension(_path);
        if (oldFilename.Contains('_'))
        {
            oldFilename = oldFilename.Split('_').First();
        }

        var newFilename = $"{oldFilename}_{DateTimetoPrefix(DateTime.Now)}.log";
        return Path.Combine(directory,newFilename);
    }

    //===========================================================//
    private static string DateTimetoPrefix(DateTime dateTime)
    {
        return dateTime.ToString("yyyyMMddHHmm");
    }

    //===========================================================//
    private static DateTime PrefixToDateTime(string prefix)
    {
        var year = Convert.ToInt32(string.Join("",prefix.Take(4)));
        var month = Convert.ToInt32(string.Join("",prefix.Skip(4).Take(2)));
        var day = Convert.ToInt32(string.Join("",prefix.Skip(6).Take(2)));
        var hour = Convert.ToInt32(string.Join("",prefix.Skip(8).Take(2)));
        var minute = Convert.ToInt32(string.Join("",prefix.Skip(10).Take(2)));

        return new DateTime(year,month,day,hour,minute,0);
    }

    //===========================================================//
    private int ConvertSizetoMb()
    {
        return Convert.ToInt32(Math.Truncate(_currentSize / 1024f / 1024f));
    }

    //===========================================================//
    public void InternalLog(string message,LogType logType = LogType.Info)
    {
        if (_truncate && ConvertSizetoMb() >= _maxSizeMb)
        {
            UpdateFile();
        }

        var sendMessage = string.Empty;
        switch (logType)
        {
            case LogType.Error:
            {
                sendMessage += "( E ) ";
                break;
            }
            case LogType.Warning:
            {
                sendMessage += "( W ) ";
                break;
            }
            case LogType.Info:
            {
                sendMessage += "( I ) ";
                break;
            }
        }
        sendMessage += $"{DateTime.Now:dd.MM.yyyy HH:mm:ss}: {message}";
        _streamWriter.WriteLine(sendMessage);
        _streamWriter.Flush();
        _currentSize += Encoding.ASCII.GetByteCount(sendMessage);
        Console.WriteLine(_currentSize);
    }

    //===========================================================//
    public void InternalLogException(Exception ex)
    {
        if (_truncate && ConvertSizetoMb() >= _maxSizeMb)
        {
            UpdateFile();
        }

        var sendMessage = $"( E ) {DateTime.Now:dd.MM.yyyy HH:mm:ss}: {ex.Message}{Environment.NewLine}{ex.StackTrace}";
        _streamWriter.WriteLine(sendMessage);
        _streamWriter.Flush();
        _currentSize += Encoding.ASCII.GetByteCount(sendMessage);
    }
}

您以前遇到过这样的问题吗?如何解决?谢谢:)

解决方法

我不知道您的应用程序每分钟会向日志写入多少数据。但是,如果数量超过10MB,则方法DateTimeToPrefix将在一分钟间隔内第二次调用返回相同的名称。 (至少对我来说,这就是Main方法中包含的代码会发生的情况。)

我将ToString()更改为还包括秒数,这可以将正确数量的数据写入期望的文件中。

private static string DateTimeToPrefix(DateTime dateTime)
{
    return dateTime.ToString("yyyyMMddHHmmss");
}
,

您的代码看起来“不错”,所以我想这个问题是由多个线程一次访问InternalLog方法引起的。您的代码不是线程安全的,因为它不使用任何锁定机制。对于您的项目,最简单且可能完全足够的解决方案是在类级别添加锁定对象:

private readonly object _lock = new object();

然后将整个InternalLog方法包装在lock(_lock)语句中:

public void InternalLog(string message,LogType logType = LogType.Info)
{
    lock(_lock) 
    {
        // your existing code
    }
}

这不是一个完美的解决方案,并且可能导致瓶颈,尤其是在每次调用StreamWriter时刷新InternalLog时。但是现在应该可以了!