123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405 |
- using System;
- using System.Collections.Concurrent;
- using System.Text;
- using Best.HTTP.Shared.PlatformSupport.Text;
- using Best.HTTP.Shared.PlatformSupport.Threading;
- namespace Best.HTTP.Shared.Logger
- {
- /// <summary>
- /// <see cref="IFilter"/> implementation to include only one division in the log output.
- /// </summary>
- public sealed class SingleDivisionFilter : IFilter
- {
- private string _division;
- public SingleDivisionFilter(string division) => this._division = division;
- public bool Include(string division) => this._division.Equals(division, StringComparison.Ordinal);
- }
- /// <summary>
- /// <see cref="IFilter"/> implementation to allow filtering for multiple divisions.
- /// </summary>
- public sealed class MultiDivisionFilter : IFilter
- {
- private string[] _divisions;
- public MultiDivisionFilter(string[] divisions)
- {
- this._divisions = divisions;
- for (int i = 0; i < this._divisions.Length; ++i)
- this._divisions[i] = this._divisions[i].Trim();
- }
- public bool Include(string division)
- {
- for (int i = 0; i < this._divisions.Length; ++i)
- {
- ref var div = ref this._divisions[i];
- if (div.Equals(division, StringComparison.Ordinal))
- return true;
- }
- return false;
- }
- }
- public sealed class ThreadedLogger : Best.HTTP.Shared.Logger.ILogger, IDisposable
- {
- public Loglevels Level { get; set; }
- public bool IsDiagnostic { get => this.Level == Loglevels.All; }
- public ILogOutput Output { get { return this._output; }
- set
- {
- if (this._output != value)
- {
- if (this._output != null)
- this._output.Dispose();
- this._output = value;
- }
- }
- }
- private ILogOutput _output;
- public IFilter Filter { get; set; }
- public bool IsEmpty
- #if !UNITY_WEBGL || UNITY_EDITOR
- => this.jobs.IsEmpty;
- #else
- => true;
- #endif
- public int InitialStringBufferCapacity = 256;
- #if !UNITY_WEBGL || UNITY_EDITOR
- public TimeSpan ExitThreadAfterInactivity = TimeSpan.FromMinutes(1);
- public int QueuedJobs { get => this.jobs.Count; }
- private ConcurrentQueue<LogJob> jobs = new ConcurrentQueue<LogJob>();
- private System.Threading.AutoResetEvent newJobEvent = new System.Threading.AutoResetEvent(false);
- private volatile int threadCreated;
- private volatile bool isDisposed;
- #endif
- private StringBuilder sb = new StringBuilder(0);
- public ThreadedLogger()
- {
- this.Level = UnityEngine.Debug.isDebugBuild ? Loglevels.Warning : Loglevels.Error;
- this.Output = new UnityOutput();
- }
- public void Verbose(string division, string msg, LoggingContext context) {
- AddJob(Loglevels.All, division, msg, null, context);
- }
- public void Information(string division, string msg, LoggingContext context) {
- AddJob(Loglevels.Information, division, msg, null, context);
- }
- public void Warning(string division, string msg, LoggingContext context) {
- AddJob(Loglevels.Warning, division, msg, null, context);
- }
- public void Error(string division, string msg, LoggingContext context) {
- AddJob(Loglevels.Error, division, msg, null, context);
- }
- public void Exception(string division, string msg, Exception ex, LoggingContext context) {
- AddJob(Loglevels.Exception, division, msg, ex, context);
- }
- private void AddJob(Loglevels level, string div, string msg, Exception ex, LoggingContext context)
- {
- if (this.Level > level)
- return;
- var filter = this.Filter;
- if (filter != null && !filter.Include(div))
- return;
- sb.EnsureCapacity(InitialStringBufferCapacity);
- #if !UNITY_WEBGL || UNITY_EDITOR
- if (this.isDisposed)
- return;
- #endif
- string json = null;
- if (context != null)
- {
- var jsonBuilder = StringBuilderPool.Get(1);
- context.ToJson(jsonBuilder);
- json = StringBuilderPool.ReleaseAndGrab(jsonBuilder);
- }
- var job = new LogJob
- {
- level = level,
- division = div,
- msg = msg,
- ex = ex,
- time = DateTime.Now,
- threadId = System.Threading.Thread.CurrentThread.ManagedThreadId,
- stackTrace = System.Environment.StackTrace,
- context = json
- };
- #if !UNITY_WEBGL || UNITY_EDITOR
- // Start the consumer thread before enqueuing to get up and running sooner
- if (System.Threading.Interlocked.CompareExchange(ref this.threadCreated, 1, 0) == 0)
- Best.HTTP.Shared.PlatformSupport.Threading.ThreadedRunner.RunLongLiving(ThreadFunc);
- this.jobs.Enqueue(job);
- try
- {
- this.newJobEvent.Set();
- }
- catch
- {
- try
- {
- this.Output.Write(job.level, job.ToJson(sb, this.Output.AcceptColor));
- }
- catch
- { }
- return;
- }
- // newJobEvent might timed out between the previous threadCreated check and newJobEvent.Set() calls closing the thread.
- // So, here we check threadCreated again and create a new thread if needed.
- if (System.Threading.Interlocked.CompareExchange(ref this.threadCreated, 1, 0) == 0)
- Best.HTTP.Shared.PlatformSupport.Threading.ThreadedRunner.RunLongLiving(ThreadFunc);
- #else
- this.Output.Write(job.level, job.ToJson(sb, this.Output.AcceptColor));
- #endif
- }
- #if !UNITY_WEBGL || UNITY_EDITOR
- private void ThreadFunc()
- {
- ThreadedRunner.SetThreadName("Best.HTTP.Logger");
- try
- {
- LogJob job;
- /*
- LogJob job = new LogJob
- {
- level = Loglevels.Information,
- division = "ThreadFunc",
- msg = "Log thread starting!",
- ex = null,
- time = DateTime.Now,
- threadId = System.Threading.Thread.CurrentThread.ManagedThreadId,
- stackTrace = null,
- context1 = null,
- context2 = null,
- context3 = null
- };
- WriteJob(ref job);
- */
- do
- {
- // Waiting for a new log-job timed out
- if (!this.newJobEvent.WaitOne(this.ExitThreadAfterInactivity))
- {
- /*
- job = new LogJob
- {
- level = Loglevels.Information,
- division = "ThreadFunc",
- msg = "Log thread quitting!",
- ex = null,
- time = DateTime.Now,
- threadId = System.Threading.Thread.CurrentThread.ManagedThreadId,
- stackTrace = null,
- context1 = null,
- context2 = null,
- context3 = null
- };
- WriteJob(ref job);
- */
- // clear StringBuilder's inner cache and exit the thread
- sb.Length = 0;
- sb.Capacity = 0;
- System.Threading.Interlocked.Exchange(ref this.threadCreated, 0);
- return;
- }
- try
- {
- int count = 0;
- while (this.jobs.TryDequeue(out job))
- {
- WriteJob(ref job);
- if (++count >= 1000)
- this.Output.Flush();
- }
- }
- finally
- {
- this.Output.Flush();
- }
- } while (!HTTPManager.IsQuitting);
- System.Threading.Interlocked.Exchange(ref this.threadCreated, 0);
- // When HTTPManager.IsQuitting is true, there is still logging that will create a new thread after the last one quit
- // and always writing a new entry about the exiting thread would be too much overhead.
- // It would also hard to know what's the last log entry because some are generated on another thread non-deterministically.
- //var lastLog = new LogJob
- //{
- // level = Loglevels.All,
- // division = "ThreadedLogger",
- // msg = "Log Processing Thread Quitting!",
- // time = DateTime.Now,
- // threadId = System.Threading.Thread.CurrentThread.ManagedThreadId,
- //};
- //
- //this.Output.WriteVerbose(lastLog.ToJson(sb));
- }
- catch
- {
- System.Threading.Interlocked.Exchange(ref this.threadCreated, 0);
- }
- }
- void WriteJob(ref LogJob job)
- {
- try
- {
- this.Output.Write(job.level, job.ToJson(sb, this.Output.AcceptColor));
- }
- catch
- { }
- }
- #endif
- public void Dispose()
- {
- #if !UNITY_WEBGL || UNITY_EDITOR
- this.isDisposed = true;
- if (this.newJobEvent != null)
- {
- this.newJobEvent.Close();
- this.newJobEvent = null;
- }
- #endif
- if (this.Output != null)
- {
- this.Output.Dispose();
- this.Output = new UnityOutput();
- }
- GC.SuppressFinalize(this);
- }
- }
- [Best.HTTP.Shared.PlatformSupport.IL2CPP.Il2CppEagerStaticClassConstructionAttribute]
- struct LogJob
- {
- private static string[] LevelStrings = new string[] { "Verbose", "Information", "Warning", "Error", "Exception" };
- public Loglevels level;
- public string division;
- public string msg;
- public Exception ex;
- public DateTime time;
- public int threadId;
- public string stackTrace;
- public string context;
- private static string WrapInColor(string str, string color, bool acceptColor)
- {
- #if UNITY_EDITOR
- return acceptColor ? string.Format("<b><color={1}>{0}</color></b>", str, color) : str;
- #else
- return str;
- #endif
- }
- public string ToJson(StringBuilder sb, bool acceptColor)
- {
- sb.Length = 0;
- sb.AppendFormat("{{\"tid\":{0},\"div\":\"{1}\",\"msg\":\"{2}\",\"t\":{3},\"ll\":\"{4}\"",
- WrapInColor(this.threadId.ToString(), "yellow", acceptColor),
- WrapInColor(this.division, "yellow", acceptColor),
- WrapInColor(LoggingContext.Escape(this.msg), "yellow", acceptColor),
- this.time.Ticks.ToString(),
- LevelStrings[(int)this.level]);
- if (ex != null)
- {
- sb.Append(",\"ex\": [");
- Exception exception = this.ex;
- while (exception != null)
- {
- sb.Append("{\"msg\": \"");
- sb.Append(LoggingContext.Escape(exception.Message));
- sb.Append("\", \"stack\": \"");
- sb.Append(LoggingContext.Escape(exception.StackTrace));
- sb.Append("\"}");
- exception = exception.InnerException;
- if (exception != null)
- sb.Append(",");
- }
- sb.Append("]");
- }
- if (this.stackTrace != null)
- {
- sb.Append(",\"stack\":\"");
- ProcessStackTrace(sb);
- sb.Append("\"");
- }
- else
- sb.Append(",\"stack\":\"\"");
- if (this.context != null)
- {
- sb.Append(",\"ctx\": [");
- sb.Append(this.context);
- sb.Append("],\"bh\":1}");
- }
- else
- sb.Append(",\"ctxs\":[],\"bh\":1}");
- return sb.ToString();
- }
- private void ProcessStackTrace(StringBuilder sb)
- {
- if (string.IsNullOrEmpty(this.stackTrace))
- return;
- var lines = this.stackTrace.Split('\n');
- // skip top 4 lines that would show the logger.
- for (int i = 3; i < lines.Length; ++i)
- sb.Append(LoggingContext.Escape(lines[i].Replace("Best.HTTP.", "")));
- }
- }
- }
|