问题描述
我正在尝试实现一个简单的文件记录器对象,有可能在文件大小达到阈值时将其截断。
我使用的是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
时。但是现在应该可以了!