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.
		
		
		
		
		
			
		
			
				
					
					
						
							387 lines
						
					
					
						
							13 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							387 lines
						
					
					
						
							13 KiB
						
					
					
				
								using Apewer.Models;
							 | 
						|
								using System;
							 | 
						|
								using System.Collections.Generic;
							 | 
						|
								using System.Diagnostics;
							 | 
						|
								using System.IO;
							 | 
						|
								using System.Text;
							 | 
						|
								
							 | 
						|
								namespace Apewer
							 | 
						|
								{
							 | 
						|
								
							 | 
						|
								    /// <summary>日志记录程序。</summary>
							 | 
						|
								    public sealed class Logger
							 | 
						|
								    {
							 | 
						|
								
							 | 
						|
								        private bool _useconsole = true;
							 | 
						|
								        private bool _usefile = true;
							 | 
						|
								        private bool _uselock = false;
							 | 
						|
								        private int _reserved = 30;
							 | 
						|
								        private LogCollector _collector = null;
							 | 
						|
								        private string _lastdate = null;
							 | 
						|
								        private Action<LogItem>[] _actions = new Action<LogItem>[0];
							 | 
						|
								        private List<Action<LogItem>> _actions_list = new List<Action<LogItem>>();
							 | 
						|
								        private object _locker = new object();
							 | 
						|
								
							 | 
						|
								        /// <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>使用自定义方法。</summary>
							 | 
						|
								        public void UseAction(Action<LogItem> action)
							 | 
						|
								        {
							 | 
						|
								            if (action != null)
							 | 
						|
								            {
							 | 
						|
								                lock (_actions_list)
							 | 
						|
								                {
							 | 
						|
								                    _actions_list.Add(action);
							 | 
						|
								                    _actions = _actions_list.ToArray();
							 | 
						|
								                }
							 | 
						|
								            }
							 | 
						|
								        }
							 | 
						|
								
							 | 
						|
								        /// <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; }
							 | 
						|
								
							 | 
						|
								        void Inbackground(Action action)
							 | 
						|
								        {
							 | 
						|
								            if (Background)
							 | 
						|
								            {
							 | 
						|
								                RuntimeUtility.InBackground(() =>
							 | 
						|
								                {
							 | 
						|
								                    lock (_locker)
							 | 
						|
								                    {
							 | 
						|
								                        action.Invoke();
							 | 
						|
								                    }
							 | 
						|
								                }, true);
							 | 
						|
								                return;
							 | 
						|
								            }
							 | 
						|
								            lock (_locker)
							 | 
						|
								            {
							 | 
						|
								                try { action.Invoke(); } catch { }
							 | 
						|
								            }
							 | 
						|
								        }
							 | 
						|
								
							 | 
						|
								        internal void Output(string tag, object[] content, Exception exception = null)
							 | 
						|
								        {
							 | 
						|
								            if (!Enabled) return;
							 | 
						|
								            Inbackground(() =>
							 | 
						|
								            {
							 | 
						|
								                var item = new LogItem();
							 | 
						|
								                item.Logger = this;
							 | 
						|
								                item.Tag = tag;
							 | 
						|
								                item.Content = content;
							 | 
						|
								                item.Exception = exception;
							 | 
						|
								
							 | 
						|
								                var text = (UseConsole || UseFile) ? item.ToString() : null;
							 | 
						|
								                if (UseConsole)
							 | 
						|
								                {
							 | 
						|
								                    lock (ConsoleLocker)
							 | 
						|
								                    {
							 | 
						|
								                        System.Console.WriteLine(text);
							 | 
						|
								                    }
							 | 
						|
								                }
							 | 
						|
								                if (UseFile)
							 | 
						|
								                {
							 | 
						|
								                    lock (_cache_locker)
							 | 
						|
								                    {
							 | 
						|
								                        if (_cache_capacity > 0)
							 | 
						|
								                        {
							 | 
						|
								                            _cache_array[_cache_count] = text;
							 | 
						|
								                            _cache_count += 1;
							 | 
						|
								                            if (_cache_count == _cache_capacity) Flush();
							 | 
						|
								                        }
							 | 
						|
								                        else
							 | 
						|
								                        {
							 | 
						|
								                            ToFile(text, this);
							 | 
						|
								                        }
							 | 
						|
								                    }
							 | 
						|
								                }
							 | 
						|
								
							 | 
						|
								                var actions = _actions;
							 | 
						|
								                foreach (var action in actions)
							 | 
						|
								                {
							 | 
						|
								                    action.Invoke(item);
							 | 
						|
								                }
							 | 
						|
								            });
							 | 
						|
								        }
							 | 
						|
								
							 | 
						|
								        /// <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 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>
							 | 
						|
								        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 fileName = (logger == null || logger.Name.IsEmpty()) ? $"{date}{FileExt}" : $"{date}_{logger.Name}{FileExt}";
							 | 
						|
								
							 | 
						|
								            // 写入文件。
							 | 
						|
								            var filePath = Path.Combine(logDir, fileName);
							 | 
						|
								            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 fileExt = Path.GetExtension(path);
							 | 
						|
								                if (fileExt != FileExt) continue;
							 | 
						|
								
							 | 
						|
								                // 文件名格式
							 | 
						|
								                var fileName = Path.GetFileName(path);
							 | 
						|
								                if (fileName[4] != '-') continue;
							 | 
						|
								                if (fileName[7] != '-') continue;
							 | 
						|
								                if (fileName[10] != '.' && fileName[10] != '_') continue;
							 | 
						|
								
							 | 
						|
								                // 确定文件名是属于当前的 Logger
							 | 
						|
								                if (logger.Name.IsEmpty())
							 | 
						|
								                {
							 | 
						|
								                    if (fileName.Length != 14) continue; // 2008-08-08.log
							 | 
						|
								                }
							 | 
						|
								                else
							 | 
						|
								                {
							 | 
						|
								                    if (fileName.Length < 16) continue; // 2008-08-08_a.log
							 | 
						|
								                    var loggerName = fileName.Substring(11, fileName.Length - 15);
							 | 
						|
								                    if (loggerName != logger.Name) continue;
							 | 
						|
								                }
							 | 
						|
								
							 | 
						|
								                // 从文件名解析日期
							 | 
						|
								                var dt = ClockUtility.Parse(fileName.Substring(0, 10));
							 | 
						|
								                if (dt == null) 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(null, true, false, true);
							 | 
						|
								        private static Logger _console = new Logger(null, true, false, true);
							 | 
						|
								        private static Logger _web = new Logger(null, true, false, true);
							 | 
						|
								
							 | 
						|
								#if DEBUG
							 | 
						|
								        internal static Logger _internals = new Logger(null, true, false, true);
							 | 
						|
								#else
							 | 
						|
								        internal static Logger _internals = new Logger(null, 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.Output(null, content, null);
							 | 
						|
								
							 | 
						|
								        /// <summary>使用 Logger.Default 写入日志,自动添加时间和日期,多个 Content 参数将以“ | ”分隔。</summary>
							 | 
						|
								        public static void Write(Exception exception, params object[] content) => Default.Output(null, content, 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
							 | 
						|
								
							 | 
						|
								        #region public
							 | 
						|
								
							 | 
						|
								
							 | 
						|
								
							 | 
						|
								        #endregion
							 | 
						|
								
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								}
							 | 
						|
								
							 |