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

StreamWriter keeps writing to the same file after instantiating it again for another file

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

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

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

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

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

我发现使用调试器一步步进行可以使它正常工作,但它不能解决我的问题。每秒记录一次似乎使程序完全跳过 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, bool truncate = false, 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, FileMode.Append, FileAccess.Write, 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);
    }
}

用法示例:

private static void Main(string[] args)
{
    Logger.Set("Log", true, 10);
    while (true)
    {
        Logger.Log("anything");
    }
}

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

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

private readonly object _lock = new object();

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

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

这不是一个完美的解决方案,可能会导致瓶颈,尤其是当您在每次调用 InternalLog 时刷新 StreamWriter。不过目前应该没问题!

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

我更改了 ToString() 以包括秒数,这给出了写入预期文件的正确数据量。

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