You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

443 lines
16 KiB

using Apewer.Internals;
using Apewer.Models;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading;
using static System.ConsoleColor;
namespace Apewer
{
/// <summary>日志记录程序。</summary>
public sealed class Logger
{
private bool _useconsole = true;
private bool _usefile = false;
private bool _uselock = false;
private int _reserved = -1;
private LogCollector _collector = null;
private string _lastdate = null;
/// <summary>当前日志记录器的名称。</summary>
public string Name { get; set; }
/// <summary>已启用。</summary>
public bool Enabled { get; set; }
/// <summary>在后台线程处理日志。默认值:FALSE。</summary>
internal bool Background { get; set; } = false;
/// <summary>使用控制台输出。默认值:TRUE。</summary>
public bool UseConsole
{
get { return _useconsole; }
set { if (!_uselock) _useconsole = value; }
}
/// <summary>使用日志文件。默认值:FALSE。</summary>
public bool UseFile
{
get { return _usefile; }
set
{
if (_uselock) return;
if (_cache_count > 0) Flush();
_usefile = value;
}
}
/// <summary>日志文件的保留天数(不包含今天)。指定 <see cref="FilePathGetter"/> 时此属性无效。</summary>
/// <remarks>默认值:-1,保留所有日志。</remarks>
public int FileReserved { get => _reserved; set => _reserved = value; }
/// <summary>设置过期日志文件的回收程序。指定 <see cref="FilePathGetter"/> 时此属性无效。</summary>
/// <remarks>默认值:NULL,删除文件。</remarks>
public LogCollector Collector { get => _collector; set => _collector = value; }
private void Invoke(Action action)
{
if (Background)
{
RuntimeUtility.InBackground(action, true);
return;
}
try { action.Invoke(); } catch { }
}
internal void Colorful(object sender, string tag, Nullable<ConsoleColor> color, object[] content, Exception exception = null)
{
if (!Enabled) return;
Invoke(() =>
{
var item = new LogItem();
item.Sender = sender;
item.Tag = tag;
item.Color = color;
item.Content = MergeContent(content);
item.Exception = exception;
if (UseConsole) ToConsole(item);
if (UseFile)
{
var plain = ToText(item);
lock (_cache_locker)
{
if (_cache_capacity > 0)
{
_cache_array[_cache_count] = plain;
_cache_count += 1;
if (_cache_count == _cache_capacity) Flush();
}
else
{
ToFile(plain, this);
}
}
}
});
}
/// <summary>记录异常。</summary>
internal void InnerException(object sender, Exception exception)
{
if (!Enabled) return;
var type = null as string;
var content = null as string;
if (exception != null)
{
try
{
type = exception.GetType().Name;
content = MergeContent(new object[] { type, exception.Message });
}
catch { }
}
if (content == null) content = "无效的 Exception 实例。";
Colorful(sender, "Exception", DarkMagenta, new object[] { type, content }, exception);
}
/// <summary>创建新实例。</summary>
public Logger()
{
Enabled = true;
}
private Logger(string name, bool useConsole, bool useFile, bool enabled)
{
Name = name;
UseConsole = useConsole;
UseFile = useFile;
Enabled = enabled;
}
#region 文件输出缓存。
private object _cache_locker = new object();
private int _cache_capacity = 0;
private int _cache_count = 0;
private string[] _cache_array = null;
/// <summary>设置缓存容量,指定为 0 可取消缓存,较大的日志缓存可能会耗尽内存。</summary>
public void SetCache(int capacity)
{
lock (_cache_locker)
{
if (_cache_count > 0) Flush();
if (capacity == _cache_capacity) return;
_cache_capacity = capacity > 0 ? capacity : 0;
_cache_array = (capacity < 1) ? null : new string[capacity];
}
}
/// <summary>将缓存的日志写入文件。</summary>
public void Flush()
{
lock (_cache_locker)
{
if (_cache_count < 1) return;
var sb = new StringBuilder();
for (var i = 0; i < _cache_count; i++)
{
sb.Append(_cache_array[i]);
sb.Append("\r\n");
}
ToFile(sb.ToString(), this, false);
_cache_count = 0;
_cache_array = new string[_cache_capacity];
}
}
#endregion
#region 输出。
internal static object FileLocker = new object();
internal static object ConsoleLocker = new object();
/// <summary>获取用于保存日志文件的路径。</summary>
public static Func<Logger, string> FilePathGetter { get; set; }
private static string MergeContent(object[] content) => TextUtility.Join(" | ", content);
private static string FormatSender(object sender)
{
if (sender == null) return null;
if (sender is string) return sender as string;
if (sender is Type) return ((Type)sender).Name;
return sender.GetType().Name;
}
// 向控制台输出。
private static void ToConsole(LogItem item)
{
var hasTag = !string.IsNullOrEmpty(item.Tag);
var sender = FormatSender(item.Sender);
var colorful = item.Color != null;
lock (ConsoleLocker)
{
if (!colorful)
{
System.Console.WriteLine(ToText(item));
return;
}
System.Console.ResetColor();
System.Console.ForegroundColor = DarkGray;
System.Console.Write(item.Clock);
System.Console.ResetColor();
if (hasTag)
{
System.Console.Write(" ");
if (item.Color != null)
{
System.Console.BackgroundColor = item.Color.Value;
System.Console.ForegroundColor = White;
}
System.Console.Write(" ");
System.Console.Write(item.Tag);
System.Console.Write(" ");
}
System.Console.ResetColor();
if (!string.IsNullOrEmpty(sender))
{
System.Console.Write(" <");
System.Console.Write(sender);
System.Console.Write(">");
}
if (!string.IsNullOrEmpty(item.Content))
{
System.Console.Write(" ");
System.Console.Write(item.Content);
}
System.Console.WriteLine();
}
}
private static string ToText(LogItem item)
{
var sb = new StringBuilder();
var sender = FormatSender(item.Sender);
sb.Append(item.Clock);
if (!string.IsNullOrEmpty(item.Tag))
{
sb.Append(" [");
sb.Append(item.Tag);
sb.Append("]");
}
if (!string.IsNullOrEmpty(sender))
{
sb.Append(" <");
sb.Append(sender);
sb.Append(">");
}
if (!string.IsNullOrEmpty(item.Content))
{
sb.Append(" ");
sb.Append(item.Content);
}
return sb.ToString();
}
// 向日志文件输出文本,文件名按日期自动生成。
private static string ToFile(string plain, Logger logger, bool crlf = true)
{
lock (FileLocker)
{
var path = GetFilePath(logger);
if (string.IsNullOrEmpty(path)) return "写入日志文件失败:无法获取日志文件路径。";
var bytes = TextUtility.Bytes(crlf ? TextUtility.Merge(plain, "\r\n") : plain);
if (!StorageUtility.AppendFile(path, bytes)) return "写入日志文件失败。";
}
return null;
}
/// <summary>获取日志文件路径发生错误时返回 NULL 值。</summary>
/// <remarks>默认例:<br/>d:\app\log\1970-01-01.log<br/>d:\www\app_data\log\1970-01-01.log</remarks>
public static string GetFilePath(Logger logger = null)
{
var getter = FilePathGetter;
if (getter != null) try { return getter.Invoke(logger); } catch { }
// 找到 App_Data 目录。
var appDir = RuntimeUtility.ApplicationPath;
var dataDir = Path.Combine(appDir, "app_data");
if (StorageUtility.DirectoryExists(dataDir)) appDir = dataDir;
// 检查 Log 目录,不存在时创建,创建失败时返回。
var logDir = Path.Combine(appDir, "log");
if (!StorageUtility.AssureDirectory(logDir)) return null;
// 文件不存在时创建新文件,无法创建时返回。
var now = DateTime.Now;
var date = now.Lucid(true, false, false, false);
var filePath = Path.Combine(logDir, date + FileExt);
if (!StorageUtility.FileExists(filePath))
{
StorageUtility.WriteFile(filePath, TextUtility.Bom);
if (!StorageUtility.FileExists(filePath)) return null;
}
// 检查过期文件。
if (logger != null && getter == null && logger._reserved > -1)
{
if (date != logger._lastdate)
{
logger._lastdate = date;
RuntimeUtility.StartThread(() => CollectFiles(logger, now, logDir));
}
}
// 返回 log 文件路径。
return filePath;
}
static void CollectFiles(Logger logger, DateTime now, string logDir)
{
var reserved = logger._reserved;
if (reserved < 0) return;
var collector = logger._collector;
var today = DateTime.Now.Date;
var paths = StorageUtility.GetSubFiles(logDir);
foreach (var path in paths)
{
var fileName = Path.GetFileNameWithoutExtension(path);
var fileExt = Path.GetExtension(path);
if (fileName.Length != 10) continue;
if (fileName[4] != '-') continue;
if (fileName[7] != '-') continue;
if (fileExt != FileExt) continue;
var dt = ClockUtility.ParseLucid(fileName);
if (dt == null || !dt.HasValue) continue;
var days = Convert.ToInt32(Convert.ToInt64((today - dt.Value).TotalMilliseconds) / 86400000L);
if (days > reserved)
{
if (collector == null) StorageUtility.DeleteFile(path);
else collector(path, days);
}
}
}
#endregion
#region 默认实列。
private static Logger _default = new Logger("Apewer.Logger.Default", true, false, true);
private static Logger _console = new Logger("Apewer.Logger.Console", true, false, true);
private static Logger _web = new Logger("Apewer.Logger.Web", true, false, true);
#if DEBUG
internal static Logger _internals = new Logger("Apewer.Logger.Internals", true, false, true);
#else
internal static Logger _internals = new Logger("Apewer.Logger.Internals", true, false, false);
#endif
/// <summary>内部的日志记录程序。</summary>
public static Logger Internals { get => _internals; }
/// <summary>默认的日志记录程序。</summary>
public static Logger Default { get => _default; }
/// <summary>仅输出到控制台的日志记录程序。</summary>
public static Logger Console { get => _console; }
/// <summary>用于 Web 的日志记录程序。</summary>
public static Logger Web { get => _web; }
#endregion
#region 静态。
const string FileExt = ".log";
/// <summary>使用 Logger.Default 写入日志,自动添加时间和日期,多个 Content 参数将以“ | ”分隔。</summary>
public static void Write(params object[] content) => Default.Colorful(null, null, null, content, null);
/// <summary>使用 Logger.Default 写入日志,自动添加时间和日期,多个 Content 参数将以“ | ”分隔。</summary>
public static void Write<T>(params object[] content) => Default.Colorful(typeof(T), null, null, content, null);
/// <summary>使用 Logger.Default 写入日志,自动添加时间和日期。</summary>
public static void Write(Exception exception) => Default.InnerException(null, exception);
/// <summary>使用 Logger.Default 写入日志,自动添加时间和日期。</summary>
public static void Write<T>(Exception exception) => Default.InnerException(typeof(T), exception);
/// <summary>压缩日志文件的内容,另存为 ZIP 文件,并删除原日志文件。</summary>
public static void CollectToZip(string path)
{
if (!File.Exists(path)) return;
var bytes = StorageUtility.ReadFile(path);
if (bytes.Length > 0)
{
var zipDict = new Dictionary<string, byte[]>();
zipDict.Add(Path.GetFileName(path), bytes);
var zipData = BytesUtility.ToZip(zipDict);
if (zipData != null && zipData.Length > 0)
{
var zipPath = path + ".zip";
StorageUtility.WriteFile(zipPath, zipData);
}
}
StorageUtility.DeleteFile(path);
}
/// <summary>压缩日志文件的内容,另存为 GZIP 文件,并删除原日志文件。</summary>
public static void CollectToGZip(string path)
{
if (!File.Exists(path)) return;
var bytes = StorageUtility.ReadFile(path);
if (bytes.Length > 0)
{
var gzipData = BytesUtility.ToGzip(bytes);
if (gzipData != null || gzipData.Length > 0)
{
var gzipPath = path + ".gzip";
StorageUtility.WriteFile(gzipPath, gzipData);
}
}
StorageUtility.DeleteFile(path);
}
#endregion
}
}