ThreadedLogger.cs 11 KB

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