ThreadedLogger.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. using System;
  2. using System.Collections.Concurrent;
  3. using System.Text;
  4. using Best.HTTP.Shared.PlatformSupport.Text;
  5. using Best.HTTP.Shared.PlatformSupport.Threading;
  6. namespace Best.HTTP.Shared.Logger
  7. {
  8. /// <summary>
  9. /// <see cref="IFilter"/> implementation to include only one division in the log output.
  10. /// </summary>
  11. public sealed class SingleDivisionFilter : IFilter
  12. {
  13. private string _division;
  14. public SingleDivisionFilter(string division) => this._division = division;
  15. public bool Include(string division) => this._division.Equals(division, StringComparison.Ordinal);
  16. }
  17. /// <summary>
  18. /// <see cref="IFilter"/> implementation to allow filtering for multiple divisions.
  19. /// </summary>
  20. public sealed class MultiDivisionFilter : IFilter
  21. {
  22. private string[] _divisions;
  23. public MultiDivisionFilter(string[] divisions)
  24. {
  25. this._divisions = divisions;
  26. for (int i = 0; i < this._divisions.Length; ++i)
  27. this._divisions[i] = this._divisions[i].Trim();
  28. }
  29. public bool Include(string division)
  30. {
  31. for (int i = 0; i < this._divisions.Length; ++i)
  32. {
  33. ref var div = ref this._divisions[i];
  34. if (div.Equals(division, StringComparison.Ordinal))
  35. return true;
  36. }
  37. return false;
  38. }
  39. }
  40. public sealed class ThreadedLogger : Best.HTTP.Shared.Logger.ILogger, IDisposable
  41. {
  42. public Loglevels Level { get; set; }
  43. public bool IsDiagnostic { get => this.Level == Loglevels.All; }
  44. public ILogOutput Output { get { return this._output; }
  45. set
  46. {
  47. if (this._output != value)
  48. {
  49. if (this._output != null)
  50. this._output.Dispose();
  51. this._output = value;
  52. }
  53. }
  54. }
  55. private ILogOutput _output;
  56. public IFilter Filter { get; set; }
  57. public bool IsEmpty
  58. #if !UNITY_WEBGL || UNITY_EDITOR
  59. => this.jobs.IsEmpty;
  60. #else
  61. => true;
  62. #endif
  63. public int InitialStringBufferCapacity = 256;
  64. #if !UNITY_WEBGL || UNITY_EDITOR
  65. public TimeSpan ExitThreadAfterInactivity = TimeSpan.FromMinutes(1);
  66. public int QueuedJobs { get => this.jobs.Count; }
  67. private ConcurrentQueue<LogJob> jobs = new ConcurrentQueue<LogJob>();
  68. private System.Threading.AutoResetEvent newJobEvent = new System.Threading.AutoResetEvent(false);
  69. private volatile int threadCreated;
  70. private volatile bool isDisposed;
  71. #endif
  72. private StringBuilder sb = new StringBuilder(0);
  73. public ThreadedLogger()
  74. {
  75. this.Level = UnityEngine.Debug.isDebugBuild ? Loglevels.Warning : Loglevels.Error;
  76. this.Output = new UnityOutput();
  77. }
  78. public void Verbose(string division, string msg, LoggingContext context) {
  79. AddJob(Loglevels.All, division, msg, null, context);
  80. }
  81. public void Information(string division, string msg, LoggingContext context) {
  82. AddJob(Loglevels.Information, division, msg, null, context);
  83. }
  84. public void Warning(string division, string msg, LoggingContext context) {
  85. AddJob(Loglevels.Warning, division, msg, null, context);
  86. }
  87. public void Error(string division, string msg, LoggingContext context) {
  88. AddJob(Loglevels.Error, division, msg, null, context);
  89. }
  90. public void Exception(string division, string msg, Exception ex, LoggingContext context) {
  91. AddJob(Loglevels.Exception, division, msg, ex, context);
  92. }
  93. private void AddJob(Loglevels level, string div, string msg, Exception ex, LoggingContext context)
  94. {
  95. if (this.Level > level)
  96. return;
  97. var filter = this.Filter;
  98. if (filter != null && !filter.Include(div))
  99. return;
  100. sb.EnsureCapacity(InitialStringBufferCapacity);
  101. #if !UNITY_WEBGL || UNITY_EDITOR
  102. if (this.isDisposed)
  103. return;
  104. #endif
  105. string json = null;
  106. if (context != null)
  107. {
  108. var jsonBuilder = StringBuilderPool.Get(1);
  109. context.ToJson(jsonBuilder);
  110. json = StringBuilderPool.ReleaseAndGrab(jsonBuilder);
  111. }
  112. var job = new LogJob
  113. {
  114. level = level,
  115. division = div,
  116. msg = msg,
  117. ex = ex,
  118. time = DateTime.Now,
  119. threadId = System.Threading.Thread.CurrentThread.ManagedThreadId,
  120. stackTrace = System.Environment.StackTrace,
  121. context = json
  122. };
  123. #if !UNITY_WEBGL || UNITY_EDITOR
  124. // Start the consumer thread before enqueuing to get up and running sooner
  125. if (System.Threading.Interlocked.CompareExchange(ref this.threadCreated, 1, 0) == 0)
  126. Best.HTTP.Shared.PlatformSupport.Threading.ThreadedRunner.RunLongLiving(ThreadFunc);
  127. this.jobs.Enqueue(job);
  128. try
  129. {
  130. this.newJobEvent.Set();
  131. }
  132. catch
  133. {
  134. try
  135. {
  136. this.Output.Write(job.level, job.ToJson(sb, this.Output.AcceptColor));
  137. }
  138. catch
  139. { }
  140. return;
  141. }
  142. // newJobEvent might timed out between the previous threadCreated check and newJobEvent.Set() calls closing the thread.
  143. // So, here we check threadCreated again and create a new thread if needed.
  144. if (System.Threading.Interlocked.CompareExchange(ref this.threadCreated, 1, 0) == 0)
  145. Best.HTTP.Shared.PlatformSupport.Threading.ThreadedRunner.RunLongLiving(ThreadFunc);
  146. #else
  147. this.Output.Write(job.level, job.ToJson(sb, this.Output.AcceptColor));
  148. #endif
  149. }
  150. #if !UNITY_WEBGL || UNITY_EDITOR
  151. private void ThreadFunc()
  152. {
  153. ThreadedRunner.SetThreadName("Best.HTTP.Logger");
  154. try
  155. {
  156. LogJob job;
  157. /*
  158. LogJob job = new LogJob
  159. {
  160. level = Loglevels.Information,
  161. division = "ThreadFunc",
  162. msg = "Log thread starting!",
  163. ex = null,
  164. time = DateTime.Now,
  165. threadId = System.Threading.Thread.CurrentThread.ManagedThreadId,
  166. stackTrace = null,
  167. context1 = null,
  168. context2 = null,
  169. context3 = null
  170. };
  171. WriteJob(ref job);
  172. */
  173. do
  174. {
  175. // Waiting for a new log-job timed out
  176. if (!this.newJobEvent.WaitOne(this.ExitThreadAfterInactivity))
  177. {
  178. /*
  179. job = new LogJob
  180. {
  181. level = Loglevels.Information,
  182. division = "ThreadFunc",
  183. msg = "Log thread quitting!",
  184. ex = null,
  185. time = DateTime.Now,
  186. threadId = System.Threading.Thread.CurrentThread.ManagedThreadId,
  187. stackTrace = null,
  188. context1 = null,
  189. context2 = null,
  190. context3 = null
  191. };
  192. WriteJob(ref job);
  193. */
  194. // clear StringBuilder's inner cache and exit the thread
  195. sb.Length = 0;
  196. sb.Capacity = 0;
  197. System.Threading.Interlocked.Exchange(ref this.threadCreated, 0);
  198. return;
  199. }
  200. try
  201. {
  202. int count = 0;
  203. while (this.jobs.TryDequeue(out job))
  204. {
  205. WriteJob(ref job);
  206. if (++count >= 1000)
  207. this.Output.Flush();
  208. }
  209. }
  210. finally
  211. {
  212. this.Output.Flush();
  213. }
  214. } while (!HTTPManager.IsQuitting);
  215. System.Threading.Interlocked.Exchange(ref this.threadCreated, 0);
  216. // When HTTPManager.IsQuitting is true, there is still logging that will create a new thread after the last one quit
  217. // and always writing a new entry about the exiting thread would be too much overhead.
  218. // It would also hard to know what's the last log entry because some are generated on another thread non-deterministically.
  219. //var lastLog = new LogJob
  220. //{
  221. // level = Loglevels.All,
  222. // division = "ThreadedLogger",
  223. // msg = "Log Processing Thread Quitting!",
  224. // time = DateTime.Now,
  225. // threadId = System.Threading.Thread.CurrentThread.ManagedThreadId,
  226. //};
  227. //
  228. //this.Output.WriteVerbose(lastLog.ToJson(sb));
  229. }
  230. catch
  231. {
  232. System.Threading.Interlocked.Exchange(ref this.threadCreated, 0);
  233. }
  234. }
  235. void WriteJob(ref LogJob job)
  236. {
  237. try
  238. {
  239. this.Output.Write(job.level, job.ToJson(sb, this.Output.AcceptColor));
  240. }
  241. catch
  242. { }
  243. }
  244. #endif
  245. public void Dispose()
  246. {
  247. #if !UNITY_WEBGL || UNITY_EDITOR
  248. this.isDisposed = true;
  249. if (this.newJobEvent != null)
  250. {
  251. this.newJobEvent.Close();
  252. this.newJobEvent = null;
  253. }
  254. #endif
  255. if (this.Output != null)
  256. {
  257. this.Output.Dispose();
  258. this.Output = new UnityOutput();
  259. }
  260. GC.SuppressFinalize(this);
  261. }
  262. }
  263. [Best.HTTP.Shared.PlatformSupport.IL2CPP.Il2CppEagerStaticClassConstructionAttribute]
  264. struct LogJob
  265. {
  266. private static string[] LevelStrings = new string[] { "Verbose", "Information", "Warning", "Error", "Exception" };
  267. public Loglevels level;
  268. public string division;
  269. public string msg;
  270. public Exception ex;
  271. public DateTime time;
  272. public int threadId;
  273. public string stackTrace;
  274. public string context;
  275. private static string WrapInColor(string str, string color, bool acceptColor)
  276. {
  277. #if UNITY_EDITOR
  278. return acceptColor ? string.Format("<b><color={1}>{0}</color></b>", str, color) : str;
  279. #else
  280. return str;
  281. #endif
  282. }
  283. public string ToJson(StringBuilder sb, bool acceptColor)
  284. {
  285. sb.Length = 0;
  286. sb.AppendFormat("{{\"tid\":{0},\"div\":\"{1}\",\"msg\":\"{2}\",\"t\":{3},\"ll\":\"{4}\"",
  287. WrapInColor(this.threadId.ToString(), "yellow", acceptColor),
  288. WrapInColor(this.division, "yellow", acceptColor),
  289. WrapInColor(LoggingContext.Escape(this.msg), "yellow", acceptColor),
  290. this.time.Ticks.ToString(),
  291. LevelStrings[(int)this.level]);
  292. if (ex != null)
  293. {
  294. sb.Append(",\"ex\": [");
  295. Exception exception = this.ex;
  296. while (exception != null)
  297. {
  298. sb.Append("{\"msg\": \"");
  299. sb.Append(LoggingContext.Escape(exception.Message));
  300. sb.Append("\", \"stack\": \"");
  301. sb.Append(LoggingContext.Escape(exception.StackTrace));
  302. sb.Append("\"}");
  303. exception = exception.InnerException;
  304. if (exception != null)
  305. sb.Append(",");
  306. }
  307. sb.Append("]");
  308. }
  309. if (this.stackTrace != null)
  310. {
  311. sb.Append(",\"stack\":\"");
  312. ProcessStackTrace(sb);
  313. sb.Append("\"");
  314. }
  315. else
  316. sb.Append(",\"stack\":\"\"");
  317. if (this.context != null)
  318. {
  319. sb.Append(",\"ctx\": [");
  320. sb.Append(this.context);
  321. sb.Append("],\"bh\":1}");
  322. }
  323. else
  324. sb.Append(",\"ctxs\":[],\"bh\":1}");
  325. return sb.ToString();
  326. }
  327. private void ProcessStackTrace(StringBuilder sb)
  328. {
  329. if (string.IsNullOrEmpty(this.stackTrace))
  330. return;
  331. var lines = this.stackTrace.Split('\n');
  332. // skip top 4 lines that would show the logger.
  333. for (int i = 3; i < lines.Length; ++i)
  334. sb.Append(LoggingContext.Escape(lines[i].Replace("Best.HTTP.", "")));
  335. }
  336. }
  337. }