经过两天修正,足够稳定,发布0204,希望作为LTS版本
大石头 编写于 2020-02-04 22:44:36
X
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Text;
using System.Threading;
using NewLife.Threading;

namespace NewLife.Log
{
    /// <summary>文本文件日志类。提供向文本文件写日志的能力</summary>
    /// <remarks>
    /// 2015-06-01 为了继承TextFileLog,增加了无参构造函数,修改了异步写日志方法为虚方法,可以进行重载
    /// </remarks>
    public class TextFileLog : Logger, IDisposable
    {
        #region 构造
        /// <summary>该构造函数没有作用,为了继承而设置</summary>
        public TextFileLog() { }

        private TextFileLog(String path, Boolean isfile, String fileFormat = null)
        {
            if (!isfile)
                LogPath = path;
            else
                LogFile = path;

            if (!fileFormat.IsNullOrEmpty())
                FileFormat = fileFormat;
            else
                FileFormat = Setting.Current.LogFileFormat;

            _Timer = new TimerX(s => CloseFile(), null, 5_000, 5_000) { Async = true };
        }

        static readonly ConcurrentDictionary<String, TextFileLog> cache = new ConcurrentDictionary<String, TextFileLog>(StringComparer.OrdinalIgnoreCase);
        /// <summary>每个目录的日志实例应该只有一个,所以采用静态创建</summary>
        /// <param name="path">日志目录或日志文件路径</param>
        /// <param name="fileFormat"></param>
        /// <returns></returns>
        public static TextFileLog Create(String path, String fileFormat = null)
        {
            if (path.IsNullOrEmpty()) path = XTrace.LogPath;
            if (path.IsNullOrEmpty()) path = Runtime.IsWeb ? "../Log" : "Log";

            var key = (path + fileFormat).ToLower();
            return cache.GetOrAdd(key, k => new TextFileLog(path, false, fileFormat));
        }

        /// <summary>每个目录的日志实例应该只有一个,所以采用静态创建</summary>
        /// <param name="path">日志目录或日志文件路径</param>
        /// <returns></returns>
        public static TextFileLog CreateFile(String path)
        {
            if (path.IsNullOrEmpty()) return Create(path);

            return cache.GetOrAdd(path, k => new TextFileLog(k, true));
        }

        /// <summary>销毁</summary>
        public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }

        /// <summary>销毁</summary>
        /// <param name="disposing"></param>
        protected virtual void Dispose(Boolean disposing)
        {
            _Timer.TryDispose();

            // 销毁前把队列日志输出
            if (!_Logs.IsEmpty) WriteFile();

            var writer = LogWriter;
            if (writer != null)
            {
                writer.TryDispose();
                LogWriter = null;
            }
        }
        #endregion

        #region 属性
        /// <summary>日志文件</summary>
        public String LogFile { get; set; }

        private String _LogPath;
        /// <summary>日志目录</summary>
        public String LogPath
        {
            get
            {
                if (String.IsNullOrEmpty(_LogPath) && !String.IsNullOrEmpty(LogFile))
                    _LogPath = Path.GetDirectoryName(LogFile).GetBasePath().EnsureEnd(Path.DirectorySeparatorChar.ToString());
                return _LogPath;
            }
            set
            {
                if (String.IsNullOrEmpty(value))
                    _LogPath = value;
                else
                    _LogPath = value.GetBasePath().EnsureEnd(Path.DirectorySeparatorChar.ToString());
            }
        }

        /// <summary>日志文件格式</summary>
        public String FileFormat { get; set; }

        /// <summary>是否当前进程的第一次写日志</summary>
        private Boolean isFirst = false;
        #endregion

        #region 内部方法
        private StreamWriter LogWriter;
        private String CurrentLogFile;

        /// <summary>初始化日志记录文件</summary>
        private StreamWriter InitLog()
        {
            var path = LogPath.EnsureDirectory(false);

            StreamWriter writer = null;
            var logfile = LogFile;
            if (!String.IsNullOrEmpty(logfile))
            {
                try
                {
                    var stream = new FileStream(logfile, FileMode.Append, FileAccess.Write, FileShare.ReadWrite);
                    writer = new StreamWriter(stream, Encoding.UTF8)
                    {
                        AutoFlush = true
                    };
                }
                catch { }
            }

            if (writer == null)
            {
                logfile = Path.Combine(path, FileFormat.F(TimerX.Now));
                var ext = Path.GetExtension(logfile);
                var i = 0;
                while (i < 10)
                {
                    try
                    {
                        var stream = new FileStream(logfile, FileMode.Append, FileAccess.Write, FileShare.ReadWrite);
                        writer = new StreamWriter(stream, Encoding.UTF8)
                        {
                            AutoFlush = true
                        };
                        break;
                    }
                    catch
                    {
                        if (logfile.EndsWith("_" + i + ext))
                            logfile = logfile.Replace("_" + i + ext, "_" + (++i) + ext);
                        else
                            logfile = logfile.Replace(ext, "_0" + ext);
                    }
                }
                CurrentLogFile = logfile;
            }
            if (writer == null) throw new XException("无法写入日志!");

            // 这里赋值,会导致log文件名不会随时间而自动改变
            //LogFile = logfile;

            if (!isFirst)
            {
                isFirst = true;

                // 通过判断LogWriter.BaseStream.Length,解决有时候日志文件为空但仍然加空行的问题
                //if (File.Exists(logfile) && LogWriter.BaseStream.Length > 0) LogWriter.WriteLine();
                // 因为指定了编码,比如UTF8,开头就会写入3个字节,所以这里不能拿长度跟0比较
                if (writer.BaseStream.Length > 10) writer.WriteLine();

                //WriteHead(writer);
                writer.Write(GetHead());
            }
            return LogWriter = writer;
        }
        #endregion

        #region 异步写日志
        private readonly TimerX _Timer;
        private readonly ConcurrentQueue<String> _Logs = new ConcurrentQueue<String>();
        private volatile Int32 _logCount;
        private Int32 _writing;
        private DateTime _NextClose;

        /// <summary>写文件</summary>
        protected virtual void WriteFile()
        {
            var writer = LogWriter;

            var now = TimerX.Now;
            var logfile = Path.Combine(LogPath, FileFormat.F(now));
            if (logfile != CurrentLogFile)
            {
                writer.TryDispose();
                writer = null;
            }

            // 初始化日志读写器
            if (writer == null) writer = InitLog();

            while (_Logs.TryDequeue(out var str))
            {
                Interlocked.Decrement(ref _logCount);

                // 写日志
                writer.WriteLine(str);
            }

            // 连续5秒没日志,就关闭
            _NextClose = now.AddSeconds(5);
        }

        /// <summary>关闭文件</summary>
        protected virtual void CloseFile()
        {
            // 同步写日志
            if (Interlocked.CompareExchange(ref _writing, 1, 0) == 0)
            {
                try
                {
                    // 处理残余
                    if (!_Logs.IsEmpty) WriteFile();

                    // 连续5秒没日志,就关闭
                    var writer = LogWriter;
                    if (writer != null && _NextClose < TimerX.Now)
                    {
                        writer.TryDispose();
                        LogWriter = null;
                    }
                }
                finally
                {
                    _writing = 0;
                }
            }
        }
        #endregion

        #region 写日志
        /// <summary>写日志</summary>
        /// <param name="level"></param>
        /// <param name="format"></param>
        /// <param name="args"></param>
        protected override void OnWrite(LogLevel level, String format, params Object[] args)
        {
            // 据@夏玉龙反馈,如果不给Log目录写入权限,日志队列积压将会导致内存暴增
            if (_logCount > 100) return;

            var e = WriteLogEventArgs.Current.Set(level);
            // 特殊处理异常对象
            if (args != null && args.Length == 1 && args[0] is Exception ex && (format.IsNullOrEmpty() || format == "{0}"))
                e = e.Set(null, ex);
            else
                e = e.Set(Format(format, args), null);

            // 推入队列
            _Logs.Enqueue(e.ToString());
            Interlocked.Increment(ref _logCount);

            // 异步写日志
            if (Interlocked.CompareExchange(ref _writing, 1, 0) == 0)
            {
                ThreadPoolX.QueueUserWorkItem(() =>
                {
                    try
                    {
                        WriteFile();
                    }
                    finally
                    {
                        _writing = 0;
                    }
                });
            }
        }
        #endregion

        #region 辅助
        /// <summary>已重载。</summary>
        /// <returns></returns>
        public override String ToString()
        {
            if (!String.IsNullOrEmpty(LogFile))
                return String.Format("{0} {1}", GetType().Name, LogFile);
            else
                return String.Format("{0} {1}", GetType().Name, LogPath);
        }
        #endregion
    }
}