[fix] 修正日志对象驻留大文本导致内存浪费的问题。XCode批量插入遇到慢查询时,输出一行18M字节的日志,一直保留在该线程的WriteLogEventArgs对象Message字段中。
大石头 编写于 2024-03-06 10:57:43 大石头 提交于 2024-03-23 22:09:59
X
namespace NewLife.Log;

/// <summary>写日志事件参数</summary>
public class WriteLogEventArgs : EventArgs
{
    #region 属性
    /// <summary>日志等级</summary>
    public LogLevel Level { get; set; }

    /// <summary>日志信息</summary>
    public String Message { get; set; }

    /// <summary>异常</summary>
    public Exception Exception { get; set; }

    /// <summary>时间</summary>
    public DateTime Time { get; set; }

    /// <summary>线程编号</summary>
    public Int32 ThreadID { get; set; }

    /// <summary>是否线程池线程</summary>
    public Boolean IsPool { get; set; }

    /// <summary>是否Web线程</summary>
    public Boolean IsWeb { get; set; }

    /// <summary>线程名</summary>
    public String ThreadName { get; set; }

    /// <summary>任务编号</summary>
    public Int32 TaskID { get; set; }
    #endregion

    #region 构造
    /// <summary>实例化一个日志事件参数</summary>
    internal WriteLogEventArgs() { }
    #endregion

    #region 线程专有实例
    /*2015-06-01 @宁波-小董
     * 将Current以及Set方法组从internal修改为Public
     * 原因是 Logger在进行扩展时,重载OnWrite需要用到该静态属性以及方法,internal无法满足扩展要求
     * */
    [ThreadStatic]
    private static WriteLogEventArgs _Current;
    /// <summary>线程专有实例。线程静态,每个线程只用一个,避免GC浪费</summary>
    public static WriteLogEventArgs Current => _Current ??= new WriteLogEventArgs();
    #endregion

    #region 方法
    /// <summary>初始化为新日志</summary>
    /// <param name="level">日志等级</param>
    /// <returns>返回自身,链式写法</returns>
    public WriteLogEventArgs Set(LogLevel level)
    {
        Level = level;

        return this;
    }

    /// <summary>初始化为新日志</summary>
    /// <param name="message">日志</param>
    /// <param name="exception">异常</param>
    /// <returns>返回自身,链式写法</returns>
    public WriteLogEventArgs Set(String message, Exception exception)
    {
        Message = message;
        Exception = exception;

        Init();

        return this;
    }

    void Init()
    {
        // todo: 如果系统使用utc时间,可以把日志时间转换为本地时间
        Time = DateTime.Now.AddHours(Setting.Current.UtcIntervalHours);
        var thread = Thread.CurrentThread;
        ThreadID = thread.ManagedThreadId;
        IsPool = thread.IsThreadPoolThread;
        ThreadName = CurrentThreadName ?? thread.Name;

        var tid = Task.CurrentId;
        TaskID = tid != null ? tid.Value : -1;

#if !__CORE__
            IsWeb = System.Web.HttpContext.Current != null;
#endif
        }

    /// <summary>重置日志事件对象,释放内存</summary>
    public void Reset()
    {
        Level = LogLevel.Info;
        Message = null;
        Exception = null;
        Time = default;
        ThreadID = 0;
        IsPool = false;
        IsWeb = false;
        ThreadName = null;
        TaskID = 0;
    }

    /// <summary>获取日志全文,并重置对象释放内存</summary>
    /// <returns></returns>
    public String GetAndReset()
    {
        var msg = ToString();
        Reset();

        return msg;
    }

    /// <summary>已重载。</summary>
    /// <returns></returns>
    public override String ToString()
    {
        if (Exception != null) Message += Exception.GetMessage();

        var name = ThreadName;
        if (name.IsNullOrEmpty()) name = TaskID >= 0 ? TaskID + "" : "-";
        if (name.EqualIgnoreCase("Threadpool worker", ".NET ThreadPool Worker")) name = TaskID >= 0 ? TaskID + "" : "P";
        if (name.EqualIgnoreCase("IO Threadpool worker")) name = "IO";
        if (name.EqualIgnoreCase(".NET Long Running Task")) name = "LongTask";

        return $"{Time:HH:mm:ss.fff} {ThreadID,2} {(IsPool ? (IsWeb ? 'W' : 'Y') : 'N')} {name} {Message}";
    }
    #endregion

    #region 日志线程名
    [ThreadStatic]
    private static String _threadName;
    /// <summary>设置当前线程输出日志时的线程名</summary>
    public static String CurrentThreadName { get => _threadName; set => _threadName = value; }
    #endregion
}