ThreadedLogger.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. using System;
  2. using System.Collections.Concurrent;
  3. using System.Text;
  4. namespace BestHTTP.Logger
  5. {
  6. [BestHTTP.PlatformSupport.IL2CPP.Il2CppSetOption(BestHTTP.PlatformSupport.IL2CPP.Option.NullChecks, false)]
  7. [BestHTTP.PlatformSupport.IL2CPP.Il2CppSetOption(BestHTTP.PlatformSupport.IL2CPP.Option.ArrayBoundsChecks, false)]
  8. [BestHTTP.PlatformSupport.IL2CPP.Il2CppSetOption(BestHTTP.PlatformSupport.IL2CPP.Option.DivideByZeroChecks, false)]
  9. public sealed class ThreadedLogger : BestHTTP.Logger.ILogger, IDisposable
  10. {
  11. public Loglevels Level { get; set; }
  12. public ILogOutput Output { get { return this._output; }
  13. set
  14. {
  15. if (this._output != value)
  16. {
  17. if (this._output != null)
  18. this._output.Dispose();
  19. this._output = value;
  20. }
  21. }
  22. }
  23. private ILogOutput _output;
  24. public int InitialStringBufferCapacity = 256;
  25. #if !UNITY_WEBGL || UNITY_EDITOR
  26. public TimeSpan ExitThreadAfterInactivity = TimeSpan.FromMinutes(1);
  27. private ConcurrentQueue<LogJob> jobs = new ConcurrentQueue<LogJob>();
  28. private System.Threading.AutoResetEvent newJobEvent = new System.Threading.AutoResetEvent(false);
  29. private volatile int threadCreated;
  30. private volatile bool isDisposed;
  31. #endif
  32. private StringBuilder sb = new StringBuilder(0);
  33. public ThreadedLogger()
  34. {
  35. this.Level = UnityEngine.Debug.isDebugBuild ? Loglevels.Warning : Loglevels.Error;
  36. this.Output = new UnityOutput();
  37. }
  38. public void Verbose(string division, string msg, LoggingContext context1 = null, LoggingContext context2 = null, LoggingContext context3 = null) {
  39. AddJob(Loglevels.All, division, msg, null, context1, context2, context3);
  40. }
  41. public void Information(string division, string msg, LoggingContext context1 = null, LoggingContext context2 = null, LoggingContext context3 = null) {
  42. AddJob(Loglevels.Information, division, msg, null, context1, context2, context3);
  43. }
  44. public void Warning(string division, string msg, LoggingContext context1 = null, LoggingContext context2 = null, LoggingContext context3 = null) {
  45. AddJob(Loglevels.Warning, division, msg, null, context1, context2, context3);
  46. }
  47. public void Error(string division, string msg, LoggingContext context1 = null, LoggingContext context2 = null, LoggingContext context3 = null) {
  48. AddJob(Loglevels.Error, division, msg, null, context1, context2, context3);
  49. }
  50. public void Exception(string division, string msg, Exception ex, LoggingContext context1 = null, LoggingContext context2 = null, LoggingContext context3 = null) {
  51. AddJob(Loglevels.Exception, division, msg, ex, context1, context2, context3);
  52. }
  53. private void AddJob(Loglevels level, string div, string msg, Exception ex, LoggingContext context1, LoggingContext context2, LoggingContext context3)
  54. {
  55. if (this.Level > level)
  56. return;
  57. sb.EnsureCapacity(InitialStringBufferCapacity);
  58. #if !UNITY_WEBGL || UNITY_EDITOR
  59. if (this.isDisposed)
  60. return;
  61. #endif
  62. var job = new LogJob
  63. {
  64. level = level,
  65. division = div,
  66. msg = msg,
  67. ex = ex,
  68. time = DateTime.Now,
  69. threadId = System.Threading.Thread.CurrentThread.ManagedThreadId,
  70. stackTrace = System.Environment.StackTrace,
  71. context1 = context1 != null ? context1.Clone() : null,
  72. context2 = context2 != null ? context2.Clone() : null,
  73. context3 = context3 != null ? context3.Clone() : null
  74. };
  75. #if !UNITY_WEBGL || UNITY_EDITOR
  76. // Start the consumer thread before enqueuing to get up and running sooner
  77. if (System.Threading.Interlocked.CompareExchange(ref this.threadCreated, 1, 0) == 0)
  78. BestHTTP.PlatformSupport.Threading.ThreadedRunner.RunLongLiving(ThreadFunc);
  79. this.jobs.Enqueue(job);
  80. try
  81. {
  82. this.newJobEvent.Set();
  83. }
  84. catch
  85. {
  86. try
  87. {
  88. this.Output.Write(job.level, job.ToJson(sb));
  89. }
  90. catch
  91. { }
  92. return;
  93. }
  94. // newJobEvent might timed out between the previous threadCreated check and newJobEvent.Set() calls closing the thread.
  95. // So, here we check threadCreated again and create a new thread if needed.
  96. if (System.Threading.Interlocked.CompareExchange(ref this.threadCreated, 1, 0) == 0)
  97. BestHTTP.PlatformSupport.Threading.ThreadedRunner.RunLongLiving(ThreadFunc);
  98. #else
  99. this.Output.Write(job.level, job.ToJson(sb));
  100. #endif
  101. }
  102. #if !UNITY_WEBGL || UNITY_EDITOR
  103. private void ThreadFunc()
  104. {
  105. System.Threading.Thread.CurrentThread.Name = "BestHTTP.Logger";
  106. try
  107. {
  108. do
  109. {
  110. // Waiting for a new log-job timed out
  111. if (!this.newJobEvent.WaitOne(this.ExitThreadAfterInactivity))
  112. {
  113. // clear StringBuilder's inner cache and exit the thread
  114. sb.Length = 0;
  115. sb.Capacity = 0;
  116. System.Threading.Interlocked.Exchange(ref this.threadCreated, 0);
  117. return;
  118. }
  119. LogJob job;
  120. while (this.jobs.TryDequeue(out job))
  121. {
  122. try
  123. {
  124. this.Output.Write(job.level, job.ToJson(sb));
  125. }
  126. catch
  127. { }
  128. }
  129. } while (!HTTPManager.IsQuitting);
  130. System.Threading.Interlocked.Exchange(ref this.threadCreated, 0);
  131. // When HTTPManager.IsQuitting is true, there is still logging that will create a new thread after the last one quit
  132. // and always writing a new entry about the exiting thread would be too much overhead.
  133. // It would also hard to know what's the last log entry because some are generated on another thread non-deterministically.
  134. //var lastLog = new LogJob
  135. //{
  136. // level = Loglevels.All,
  137. // division = "ThreadedLogger",
  138. // msg = "Log Processing Thread Quitting!",
  139. // time = DateTime.Now,
  140. // threadId = System.Threading.Thread.CurrentThread.ManagedThreadId,
  141. //};
  142. //
  143. //this.Output.WriteVerbose(lastLog.ToJson(sb));
  144. }
  145. catch
  146. {
  147. System.Threading.Interlocked.Exchange(ref this.threadCreated, 0);
  148. }
  149. }
  150. #endif
  151. public void Dispose()
  152. {
  153. #if !UNITY_WEBGL || UNITY_EDITOR
  154. this.isDisposed = true;
  155. if (this.newJobEvent != null)
  156. {
  157. this.newJobEvent.Close();
  158. this.newJobEvent = null;
  159. }
  160. #endif
  161. if (this.Output != null)
  162. {
  163. this.Output.Dispose();
  164. this.Output = new UnityOutput();
  165. }
  166. GC.SuppressFinalize(this);
  167. }
  168. }
  169. [BestHTTP.PlatformSupport.IL2CPP.Il2CppEagerStaticClassConstructionAttribute]
  170. struct LogJob
  171. {
  172. private static string[] LevelStrings = new string[] { "Verbose", "Information", "Warning", "Error", "Exception" };
  173. public Loglevels level;
  174. public string division;
  175. public string msg;
  176. public Exception ex;
  177. public DateTime time;
  178. public int threadId;
  179. public string stackTrace;
  180. public LoggingContext context1;
  181. public LoggingContext context2;
  182. public LoggingContext context3;
  183. private static string WrapInColor(string str, string color)
  184. {
  185. #if UNITY_EDITOR
  186. return string.Format("<b><color={1}>{0}</color></b>", str, color);
  187. #else
  188. return str;
  189. #endif
  190. }
  191. public string ToJson(StringBuilder sb)
  192. {
  193. sb.Length = 0;
  194. sb.AppendFormat("{{\"tid\":{0},\"div\":\"{1}\",\"msg\":\"{2}\"",
  195. WrapInColor(this.threadId.ToString(), "yellow"),
  196. WrapInColor(this.division, "yellow"),
  197. WrapInColor(LoggingContext.Escape(this.msg), "yellow"));
  198. if (ex != null)
  199. {
  200. sb.Append(",\"ex\": [");
  201. Exception exception = this.ex;
  202. while (exception != null)
  203. {
  204. sb.Append("{\"msg\": \"");
  205. sb.Append(LoggingContext.Escape(exception.Message));
  206. sb.Append("\", \"stack\": \"");
  207. sb.Append(LoggingContext.Escape(exception.StackTrace));
  208. sb.Append("\"}");
  209. exception = exception.InnerException;
  210. if (exception != null)
  211. sb.Append(",");
  212. }
  213. sb.Append("]");
  214. }
  215. if (this.stackTrace != null)
  216. {
  217. sb.Append(",\"stack\":\"");
  218. ProcessStackTrace(sb);
  219. sb.Append("\"");
  220. }
  221. else
  222. sb.Append(",\"stack\":\"\"");
  223. if (this.context1 != null || this.context2 != null || this.context3 != null)
  224. {
  225. sb.Append(",\"ctxs\":[");
  226. if (this.context1 != null)
  227. this.context1.ToJson(sb);
  228. if (this.context2 != null)
  229. {
  230. if (this.context1 != null)
  231. sb.Append(",");
  232. this.context2.ToJson(sb);
  233. }
  234. if (this.context3 != null)
  235. {
  236. if (this.context1 != null || this.context2 != null)
  237. sb.Append(",");
  238. this.context3.ToJson(sb);
  239. }
  240. sb.Append("]");
  241. }
  242. else
  243. sb.Append(",\"ctxs\":[]");
  244. sb.AppendFormat(",\"t\":{0},\"ll\":\"{1}\",",
  245. this.time.Ticks.ToString(),
  246. LevelStrings[(int)this.level]);
  247. sb.Append("\"bh\":1}");
  248. return sb.ToString();
  249. }
  250. private void ProcessStackTrace(StringBuilder sb)
  251. {
  252. if (string.IsNullOrEmpty(this.stackTrace))
  253. return;
  254. var lines = this.stackTrace.Split('\n');
  255. // skip top 4 lines that would show the logger.
  256. for (int i = 3; i < lines.Length; ++i)
  257. sb.Append(LoggingContext.Escape(lines[i].Replace("BestHTTP.", "")));
  258. }
  259. }
  260. }