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

namespace NewLife.Log
{
    /// <summary>网络日志</summary>
    public class NetworkLog : Logger, IDisposable
    {
        /// <summary>服务端</summary>
        public String Server { get; set; }

        /// <summary>应用标识</summary>
        public String AppId { get; set; }

        /// <summary>客户端标识</summary>
        public String ClientId { get; set; }

        private ISocketRemote _client;
        //private HttpClient _http;
        private readonly ConcurrentQueue<String> _Logs = new();
        private volatile Int32 _logCount;
        private Int32 _writing;

        /// <summary>实例化网络日志。默认广播到514端口</summary>
        public NetworkLog() => Server = new NetUri(NetType.Udp, IPAddress.Broadcast, 514) + "";

        /// <summary>指定日志服务器地址来实例化网络日志</summary>
        /// <param name="server"></param>
        public NetworkLog(String server) => Server = server;

        /// <summary>销毁</summary>
        public void Dispose()
        {
            // 销毁前把队列日志输出
            if (_logCount > 0)
            {
                if (Interlocked.CompareExchange(ref _writing, 1, 0) == 0)
                    PushLog();
                else
                    Thread.Sleep(500);
            }

            _client.TryDispose();
            //_http.TryDispose();
        }

        private void Send(String value)
        {
            if (_client != null)
                _client.Send(value);
            //else
            //    _http?.PostAsync("", new StringContent(value)).Wait();
        }

        private Boolean _inited;
        private void Init()
        {
            if (_inited) return;

            //var sys = SysConfig.Current;
            if (AppId.IsNullOrEmpty()) AppId = AssemblyX.Entry.Name;
            if (ClientId.IsNullOrEmpty())
            {
                try
                {
                    ClientId = NetHelper.MyIP() + "@" + Process.GetCurrentProcess().Id;
                }
                catch
                {
                    ClientId = Rand.NextString(8);
                }
            }

            var uri = new NetUri(Server);
            switch (uri.Type)
            {
                case NetType.Unknown:
                    break;
                case NetType.Tcp:
                case NetType.Udp:
                    _client = uri.CreateRemote();
                    break;
//                case NetType.Http:
//                case NetType.Https:
//                case NetType.WebSocket:
//                    var handler = new HttpClientHandler { UseProxy = false };
//                    if (Net.Setting.Current.EnableHttpCompression)
//                    {
//#if NETCOREAPP3_0_OR_GREATER
//                        if (handler.SupportsAutomaticDecompression) handler.AutomaticDecompression = DecompressionMethods.All;
//#else
//                        if (handler.SupportsAutomaticDecompression) handler.AutomaticDecompression = DecompressionMethods.GZip;
//#endif
//                    }
//                    var http = new HttpClient(handler)
//                    {
//                        BaseAddress = new Uri(Server)
//                    };
//                    http.DefaultRequestHeaders.Add("X-AppId", AppId);
//                    http.DefaultRequestHeaders.Add("X-ClientId", ClientId);

//                    // 默认UserAgent
//                    http.SetUserAgent();

//                    _http = http;
//                    break;
                default:
                    break;
            }
            if (_client == null /*&& _http == null*/) return;

            // 首先发送日志头
            Send(GetHead());

            _inited = true;
        }

        /// <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)
        {
            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.GetAndReset());
        Interlocked.Increment(ref _logCount);

            // 异步写日志,实时。即使这里错误,定时器那边仍然会补上
            if (Interlocked.CompareExchange(ref _writing, 1, 0) == 0)
            {
                ThreadPool.UnsafeQueueUserWorkItem(s =>
                {
                    try
                    {
                        PushLog();
                    }
                    catch { }
                    finally
                    {
                        _writing = 0;
                    }
                }, null);
            }
        }

        private void PushLog()
        {
            Init();

            // Tcp/Udp 和 Http 推送日志时需要不同的包大小
            var max = /*_http != null ? 8192 :*/ 1460;

            var sb = new StringBuilder();
            while (_Logs.TryDequeue(out var msg))
            {
                Interlocked.Decrement(ref _logCount);

                if (sb.Length > 0 && sb.Length + msg.Length >= max)
                {
                    Send(sb.ToString());
                    //sb.Clear();
                    sb.Length = 0;
                }

                if (sb.Length > 0) sb.AppendLine();
                sb.Append(msg);
            }

            if (sb.Length > 0) Send(sb.ToString());
        }
    }
}