forked from bbbscarter/UberLogger
-
Notifications
You must be signed in to change notification settings - Fork 0
/
UberLogger.cs
705 lines (606 loc) · 26.7 KB
/
UberLogger.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
using UnityEngine;
using System.Collections.Generic;
using System.Diagnostics;
using System;
using System.Reflection;
using System.Text.RegularExpressions;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace UberLogger
{
//Use this to exclude methods from the stack trace
[AttributeUsage(AttributeTargets.Method)]
public class StackTraceIgnore : Attribute {}
//Use this to stop UberLogger handling logs with this in the callstack.
[AttributeUsage(AttributeTargets.Method)]
public class LogUnityOnly : Attribute {}
public enum LogSeverity
{
Message,
Warning,
Error,
}
/// <summary>
/// Interface for deriving new logger backends.
/// Add a new logger via Logger.AddLogger()
/// </summary>
public interface ILogger
{
/// <summary>
/// Logging backend entry point. logInfo contains all the information about the logging request.
/// </summary>
void Log(LogInfo logInfo);
}
/// <summary>
/// Interface for implementing new log message filter methods.
/// Filters will be applied to log messages before they are forwarded to any loggers or Unity itself.
/// </summary>
public interface IFilter
{
/// <summary>
/// Apply filter to log message.
/// Should return true if the message is to be kept, and false if the message is to be silenced.
/// </summary>
bool ApplyFilter(string channel, UnityEngine.Object source, LogSeverity severity, object message, params object[] par);
}
//Information about a particular frame of a callstack
[System.Serializable]
public class LogStackFrame
{
public string MethodName;
public string DeclaringType;
public string ParameterSig;
public int LineNumber;
public string FileName;
string FormattedMethodNameWithFileName;
string FormattedMethodName;
string FormattedFileName;
/// <summary>
/// Convert from a .Net stack frame
/// </summary>
public LogStackFrame(StackFrame frame)
{
var method = frame.GetMethod();
MethodName = method.Name;
DeclaringType = method.DeclaringType.FullName;
var pars = method.GetParameters();
for (int c1=0; c1<pars.Length; c1++)
{
ParameterSig += String.Format("{0} {1}", pars[c1].ParameterType, pars[c1].Name);
if(c1+1<pars.Length)
{
ParameterSig += ", ";
}
}
FileName = frame.GetFileName();
LineNumber = frame.GetFileLineNumber();
MakeFormattedNames();
}
/// <summary>
/// Convert from a Unity stack frame (for internal Unity errors rather than excpetions)
/// </summary>
public LogStackFrame(string unityStackFrame)
{
if(Logger.ExtractInfoFromUnityStackInfo(unityStackFrame, ref DeclaringType, ref MethodName, ref FileName, ref LineNumber))
{
MakeFormattedNames();
}
else
{
FormattedMethodNameWithFileName = unityStackFrame;
FormattedMethodName = unityStackFrame;
FormattedFileName = unityStackFrame;
}
}
/// <summary>
/// Basic stack frame info when we have nothing else
/// </summary>
public LogStackFrame(string message, string filename, int lineNumber)
{
FileName = filename;
LineNumber = lineNumber;
FormattedMethodNameWithFileName = message;
FormattedMethodName = message;
FormattedFileName = message;
}
public string GetFormattedMethodNameWithFileName()
{
return FormattedMethodNameWithFileName;
}
public string GetFormattedMethodName()
{
return FormattedMethodName;
}
public string GetFormattedFileName()
{
return FormattedFileName;
}
/// <summary>
/// Make a nice string showing the stack information - used by the loggers
/// </summary>
void MakeFormattedNames()
{
FormattedMethodName = String.Format("{0}.{1}({2})", DeclaringType, MethodName, ParameterSig);
string filename = FileName;
if(!String.IsNullOrEmpty(FileName))
{
var startSubName = FileName.IndexOf("Assets", StringComparison.OrdinalIgnoreCase);
if(startSubName>0)
{
filename = FileName.Substring(startSubName);
}
}
FormattedFileName = String.Format("{0}:{1}", filename, LineNumber);
FormattedMethodNameWithFileName = String.Format("{0} (at {1})", FormattedMethodName, FormattedFileName);
}
}
/// <summary>
/// A single item of logging information
/// </summary>
[System.Serializable]
public class LogInfo
{
public UnityEngine.Object Source;
public string Channel;
public LogSeverity Severity;
public string Message;
public List<LogStackFrame> Callstack;
public LogStackFrame OriginatingSourceLocation;
public double RelativeTimeStamp;
string RelativeTimeStampAsString;
public DateTime AbsoluteTimeStamp;
string AbsoluteTimeStampAsString;
public string GetRelativeTimeStampAsString()
{
return RelativeTimeStampAsString;
}
public string GetAbsoluteTimeStampAsString()
{
return AbsoluteTimeStampAsString;
}
public LogInfo(UnityEngine.Object source, string channel, LogSeverity severity, List<LogStackFrame> callstack, LogStackFrame originatingSourceLocation, object message, params object[] par)
{
Source = source;
Channel = channel;
Severity = severity;
Message = "";
OriginatingSourceLocation = originatingSourceLocation;
var messageString = message as String;
if(messageString!=null)
{
if(par.Length>0)
{
Message = System.String.Format(messageString, par);
}
else
{
Message = messageString;
}
}
else
{
if(message!=null)
{
Message = message.ToString();
}
}
Callstack = callstack;
RelativeTimeStamp = Logger.GetRelativeTime();
AbsoluteTimeStamp = DateTime.UtcNow;
RelativeTimeStampAsString = String.Format("{0:0.0000}", RelativeTimeStamp);
AbsoluteTimeStampAsString = AbsoluteTimeStamp.ToString("yyyy-MM-dd HH:mm:ss.fff", System.Globalization.CultureInfo.InvariantCulture);
}
}
/// <summary>
/// The core of UberLogger - the entry point for logging information
/// </summary>
public static class Logger
{
// Controls how many historical messages to keep to pass into newly registered loggers
public static int MaxMessagesToKeep = 1000;
// If true, any logs sent to UberLogger will be forwarded on to the Unity logger.
// Useful if you want to use both systems
public static bool ForwardMessages = true;
// Unity uses \n for line termination in internal multi-line strings. Use this instead of
// System.Environment.NewLine when you want to split multi-line strings which are emitted
// by Unity's internal APIs.
public static string UnityInternalNewLine = "\n";
// Unity uses forward-slashes as directory separators, regardless of OS.
// Convert from this separator to System.IO.Path.DirectorySeparatorChar before passing any Unity-originated
// paths to APIs which expect OS-native paths.
public static char UnityInternalDirectorySeparator = '/';
static List<ILogger> Loggers = new List<ILogger>();
static LinkedList<LogInfo> RecentMessages = new LinkedList<LogInfo>();
static long StartTick;
static bool AlreadyLogging = false;
static Regex UnityMessageRegex;
static List<IFilter> Filters = new List<IFilter>();
static Logger()
{
// Register with Unity's logging system
// _OR_NEWER only available from 5.3+
#if UNITY_5 || UNITY_5_3_OR_NEWER
Application.logMessageReceivedThreaded += UnityLogHandler;
#else
Application.RegisterLogCallback(UnityLogHandler);
#endif
StartTick = DateTime.Now.Ticks;
UnityMessageRegex = new Regex(@"^([^\(]*)\((\d+)[^\)]*\)");
}
/// <summary>
/// Registered Unity error handler
/// </summary>
[StackTraceIgnore]
static void UnityLogHandler(string logString, string stackTrace, UnityEngine.LogType logType)
{
UnityLogInternal(logString, stackTrace, logType);
}
static public double GetRelativeTime()
{
long ticks = DateTime.Now.Ticks;
return TimeSpan.FromTicks(ticks - StartTick).TotalSeconds;
}
/// <summary>
/// Registers a new logger backend, which we be told every time there's a new log.
/// if populateWithExistingMessages is true, UberLogger will immediately pump the new logger with past messages
/// </summary>
static public void AddLogger(ILogger logger, bool populateWithExistingMessages=true)
{
lock(Loggers)
{
if(populateWithExistingMessages)
{
foreach(var oldLog in RecentMessages)
{
logger.Log(oldLog);
}
}
if(!Loggers.Contains(logger))
{
Loggers.Add(logger);
}
}
}
/// <summary>
/// Registers a new filter mechanism, which will be able to silence any future log messages
/// </summary>
static public void AddFilter(IFilter filter)
{
lock (Loggers)
{
Filters.Add(filter);
}
}
/// <summary>
/// Paths provided by Unity will contain forward slashes as directory separators on all OSes.
/// This method changes all forward slashes to OS-specific directory separators.
/// </summary>
static public string ConvertDirectorySeparatorsFromUnityToOS(string unityFileName)
{
return unityFileName.Replace(UnityInternalDirectorySeparator, System.IO.Path.DirectorySeparatorChar);
}
/// <summary>
/// Tries to extract useful information about the log from a Unity error message.
/// Only used when handling a Unity error message and we can't get a useful callstack
/// </summary>
static public bool ExtractInfoFromUnityMessage(string log, ref string filename, ref int lineNumber)
{
// log = "Assets/Code/Debug.cs(140,21): warning CS0618: 'some error'
var firstMatch = UnityMessageRegex.Match(log);
if (firstMatch.Success)
{
filename = firstMatch.Groups[1].Value;
lineNumber = Convert.ToInt32(firstMatch.Groups[2].Value);
return true;
}
return false;
}
/// <summary>
/// Tries to extract useful information about the log from a Unity stack trace
/// </summary>
static public bool ExtractInfoFromUnityStackInfo(string log, ref string declaringType, ref string methodName, ref string filename, ref int lineNumber)
{
// log = "DebugLoggerEditorWindow.DrawLogDetails () (at Assets/Code/Editor.cs:298)";
var match = System.Text.RegularExpressions.Regex.Matches(log, @"(.*)\.(.*)\s*\(.*\(at (.*):(\d+)");
if(match.Count>0)
{
declaringType = match[0].Groups[1].Value;
methodName = match[0].Groups[2].Value;
filename = match[0].Groups[3].Value;
lineNumber = Convert.ToInt32(match[0].Groups[4].Value);
return true;
}
return false;
}
struct IgnoredUnityMethod
{
public enum Mode { Show, ShowIfFirstIgnoredMethod, Hide };
public string DeclaringTypeName;
public string MethodName;
public Mode ShowHideMode;
}
// Example callstack when invoking Debug.LogWarning under Unity 5.5:
// Application.CallLogCallback
// DebugLogHandler.Internal_Log
// DebugLogHandler.LogFormat
// Logger.Log
// Debug.LogWarning
// <application callstack>
static IgnoredUnityMethod[] IgnoredUnityMethods = new IgnoredUnityMethod[]
{
// Internal trampoline, which invokes UberLogger's log callback
new IgnoredUnityMethod { DeclaringTypeName = "Application", MethodName = "CallLogCallback", ShowHideMode = IgnoredUnityMethod.Mode.Hide },
// Internal log-handling methods in Unity
new IgnoredUnityMethod { DeclaringTypeName = "DebugLogHandler", MethodName = null, ShowHideMode = IgnoredUnityMethod.Mode.Hide },
// There are several entry points to Logger. These are primarily called by Unity's own code, but could also be called directly by 3rd party code.
// These are helpful to have on the callstack in case source code is not available (they help pinpoint the exact source code location that printed the message),
// but remaining ignored methods can safely be hidden
new IgnoredUnityMethod { DeclaringTypeName = "Logger", MethodName = null, ShowHideMode = IgnoredUnityMethod.Mode.ShowIfFirstIgnoredMethod },
// Many of the Debug.* entry points result in log messages being printed
// These are helpful to have on the callstack in case source code is not available (they help pinpoint the exact source code location that printed the message),
// but remaining ignored methods can safely be hidden
new IgnoredUnityMethod { DeclaringTypeName = "Debug", MethodName = null, ShowHideMode = IgnoredUnityMethod.Mode.ShowIfFirstIgnoredMethod },
// Many of the Assert.* entry points result in log messages being printed
// These are not helpful having on the callstack
// These are helpful to have on the callstack in case source code is not available (they help pinpoint the exact source code location that printed the message),
// but remaining ignored methods can safely be hidden
new IgnoredUnityMethod { DeclaringTypeName = "Assert", MethodName = null, ShowHideMode = IgnoredUnityMethod.Mode.ShowIfFirstIgnoredMethod },
};
/// <summary>
/// Identify a number of Unity methods which we would like to scrub from a stacktrace
/// Returns true if the method is part of scrubbing
/// </summary>
static IgnoredUnityMethod.Mode ShowOrHideMethod(MethodBase method)
{
foreach (IgnoredUnityMethod ignoredUnityMethod in IgnoredUnityMethods)
{
if ((method.DeclaringType.Name == ignoredUnityMethod.DeclaringTypeName) && ((ignoredUnityMethod.MethodName == null) || (method.Name == ignoredUnityMethod.MethodName)))
{
return ignoredUnityMethod.ShowHideMode;
}
}
return IgnoredUnityMethod.Mode.Show;
}
/// <summary>
/// Converts the curent stack trace into a list of UberLogger's LogStackFrame.
/// Excludes any methods with the StackTraceIgnore attribute
/// Excludes all methods (bar the first, sometimes) from a pre-defined list of ignored Unity methods/classes
/// Returns false if the stack frame contains any methods flagged as LogUnityOnly
/// </summary>
[StackTraceIgnore]
static bool GetCallstack(ref List<LogStackFrame> callstack, out LogStackFrame originatingSourceLocation)
{
callstack.Clear();
StackTrace stackTrace = new StackTrace(true); // get call stack
StackFrame[] stackFrames = stackTrace.GetFrames(); // get method calls (frames)
bool encounteredIgnoredMethodPreviously = false;
originatingSourceLocation = null;
// Iterate backwards over stackframes; this enables us to show the "first" ignored Unity method if need be, but hide subsequent ones
for (int i = stackFrames.Length - 1; i >= 0; i--)
{
StackFrame stackFrame = stackFrames[i];
var method = stackFrame.GetMethod();
if(method.IsDefined(typeof(LogUnityOnly), true))
{
return true;
}
if(!method.IsDefined(typeof(StackTraceIgnore), true))
{
IgnoredUnityMethod.Mode showHideMode = ShowOrHideMethod(method);
bool setOriginatingSourceLocation = (showHideMode == IgnoredUnityMethod.Mode.Show);
if (showHideMode == IgnoredUnityMethod.Mode.ShowIfFirstIgnoredMethod)
{
// "show if first ignored" methods are part of the stack trace only if no other ignored methods have been encountered already
if (!encounteredIgnoredMethodPreviously)
{
encounteredIgnoredMethodPreviously = true;
showHideMode = IgnoredUnityMethod.Mode.Show;
}
else
showHideMode = IgnoredUnityMethod.Mode.Hide;
}
if (showHideMode == IgnoredUnityMethod.Mode.Show)
{
var logStackFrame = new LogStackFrame(stackFrame);
callstack.Add(logStackFrame);
if (setOriginatingSourceLocation)
originatingSourceLocation = logStackFrame;
}
}
}
// Callstack has been processed backwards -- correct order for presentation
callstack.Reverse();
return false;
}
/// <summary>
/// Converts a Unity callstack string into a list of UberLogger's LogStackFrame
/// Doesn't do any filtering, since this should only be dealing with internal Unity errors rather than client code
/// </summary>
static List<LogStackFrame> GetCallstackFromUnityLog(string unityCallstack, out LogStackFrame originatingSourceLocation)
{
var lines = System.Text.RegularExpressions.Regex.Split(unityCallstack, UberLogger.Logger.UnityInternalNewLine);
var stack = new List<LogStackFrame>();
foreach(var line in lines)
{
var frame = new LogStackFrame(line);
if(!string.IsNullOrEmpty(frame.GetFormattedMethodNameWithFileName()))
{
stack.Add(new LogStackFrame(line));
}
}
if (stack.Count > 0)
originatingSourceLocation = stack[0];
else
originatingSourceLocation = null;
return stack;
}
/// <summary>
/// The core entry point of all logging coming from Unity. Takes a log request, creates the call stack and pumps it to all the backends
/// </summary>
[StackTraceIgnore()]
static void UnityLogInternal(string unityMessage, string unityCallStack, UnityEngine.LogType logType)
{
//Make sure only one thread can do this at a time.
//This should mean that most backends don't have to worry about thread safety (unless they do complicated stuff)
lock(Loggers)
{
//Prevent nasty recursion problems
if(!AlreadyLogging)
{
try
{
AlreadyLogging = true;
var callstack = new List<LogStackFrame>();
LogStackFrame originatingSourceLocation;
var unityOnly = GetCallstack(ref callstack, out originatingSourceLocation);
if(unityOnly)
{
return;
}
//If we have no useful callstack, fall back to parsing Unity's callstack
if(callstack.Count==0)
{
callstack = GetCallstackFromUnityLog(unityCallStack, out originatingSourceLocation);
}
LogSeverity severity;
switch(logType)
{
case UnityEngine.LogType.Error: severity = LogSeverity.Error; break;
case UnityEngine.LogType.Assert: severity = LogSeverity.Error; break;
case UnityEngine.LogType.Exception: severity = LogSeverity.Error; break;
case UnityEngine.LogType.Warning: severity = LogSeverity.Warning; break;
default: severity = LogSeverity.Message; break;
}
string filename = "";
int lineNumber = 0;
//Finally, parse the error message so we can get basic file and line information
if(ExtractInfoFromUnityMessage(unityMessage, ref filename, ref lineNumber))
{
callstack.Insert(0, new LogStackFrame(unityMessage, filename, lineNumber));
}
var logInfo = new LogInfo(null, "", severity, callstack, originatingSourceLocation, unityMessage);
//Add this message to our history
RecentMessages.AddLast(logInfo);
//Make sure our history doesn't get too big
TrimOldMessages();
//Delete any dead loggers and pump them with the new log
Loggers.RemoveAll(l=>l==null);
Loggers.ForEach(l=>l.Log(logInfo));
}
finally
{
AlreadyLogging = false;
}
}
}
}
/// <summary>
/// The core entry point of all logging coming from client code.
/// Takes a log request, creates the call stack and pumps it to all the backends
/// </summary>
[StackTraceIgnore()]
static public void Log(string channel, UnityEngine.Object source, LogSeverity severity, object message, params object[] par)
{
lock(Loggers)
{
if(!AlreadyLogging)
{
try
{
AlreadyLogging = true;
foreach (IFilter filter in Filters)
{
if (!filter.ApplyFilter(channel, source, severity, message, par))
return;
}
var callstack = new List<LogStackFrame>();
LogStackFrame originatingSourceLocation;
var unityOnly = GetCallstack(ref callstack, out originatingSourceLocation);
if(unityOnly)
{
return;
}
var logInfo = new LogInfo(source, channel, severity, callstack, originatingSourceLocation, message, par);
//Add this message to our history
RecentMessages.AddLast(logInfo);
//Make sure our history doesn't get too big
TrimOldMessages();
//Delete any dead loggers and pump them with the new log
Loggers.RemoveAll(l=>l==null);
Loggers.ForEach(l=>l.Log(logInfo));
//If required, pump this message back into Unity
if(ForwardMessages)
{
ForwardToUnity(source, severity, message, par);
}
}
finally
{
AlreadyLogging = false;
}
}
}
}
/// <summary>
/// Forwards an UberLogger log to Unity so it's visible in the built-in console
/// </summary>
[LogUnityOnly()]
static void ForwardToUnity(UnityEngine.Object source, LogSeverity severity, object message, params object[] par)
{
object showObject = null;
if(message!=null)
{
var messageAsString = message as string;
if(messageAsString!=null)
{
if(par.Length>0)
{
showObject = String.Format(messageAsString, par);
}
else
{
showObject = message;
}
}
else
{
showObject = message;
}
}
if(source==null)
{
if(severity==LogSeverity.Message) UnityEngine.Debug.Log(showObject);
else if(severity==LogSeverity.Warning) UnityEngine.Debug.LogWarning(showObject);
else if(severity==LogSeverity.Error) UnityEngine.Debug.LogError(showObject);
}
else
{
if(severity==LogSeverity.Message) UnityEngine.Debug.Log(showObject, source);
else if(severity==LogSeverity.Warning) UnityEngine.Debug.LogWarning(showObject, source);
else if(severity==LogSeverity.Error) UnityEngine.Debug.LogError(showObject, source);
}
}
/// <summary>
/// Finds a registered logger, if it exists
/// </summary>
static public T GetLogger<T>() where T:class
{
foreach(var logger in Loggers)
{
if(logger is T)
{
return logger as T;
}
}
return null;
}
static void TrimOldMessages()
{
while(RecentMessages.Count > MaxMessagesToKeep)
{
RecentMessages.RemoveFirst();
}
}
}
}