-
Notifications
You must be signed in to change notification settings - Fork 436
/
Copy pathLogger.cs
522 lines (441 loc) · 20.9 KB
/
Logger.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
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using osu.Framework.Platform;
using System.Linq;
using System.Threading;
using osu.Framework.Development;
using osu.Framework.Threading;
namespace osu.Framework.Logging
{
/// <summary>
/// This class allows statically (globally) configuring and using logging functionality.
/// </summary>
public class Logger
{
private static readonly object static_sync_lock = new object();
// separate locking object for flushing so that we don't lock too long on the staticSyncLock object, since we have to
// hold this lock for the entire duration of the flush (waiting for I/O etc) before we can resume scheduling logs
// but other operations like GetLogger(), ApplyFilters() etc. can still be executed while a flush is happening.
private static readonly object flush_sync_lock = new object();
/// <summary>
/// Whether logging is enabled. Setting this to false will disable all logging.
/// </summary>
public static bool Enabled = true;
/// <summary>
/// The minimum log-level a logged message needs to have to be logged. Default is <see cref="LogLevel.Verbose"/>. Please note that setting this to <see cref="LogLevel.Debug"/> will log input events, including keypresses when entering a password.
/// </summary>
public static LogLevel Level = DebugUtils.IsDebugBuild ? LogLevel.Debug : LogLevel.Verbose;
/// <summary>
/// An identifier used in log file headers to figure where the log file came from.
/// </summary>
public static string UserIdentifier = Environment.UserName;
/// <summary>
/// An identifier for the game written to log file headers to indicate where the log file came from.
/// </summary>
public static string GameIdentifier = @"game";
/// <summary>
/// An identifier for the version written to log file headers to indicate where the log file came from.
/// </summary>
public static string VersionIdentifier = @"unknown";
private static Storage storage;
/// <summary>
/// The storage to place logs inside.
/// </summary>
public static Storage Storage
{
private get => storage;
set => storage = value ?? throw new ArgumentNullException(nameof(value));
}
/// <summary>
/// Add a plain-text phrase which should always be filtered from logs. The filtered phrase will be replaced with asterisks (*).
/// Useful for avoiding logging of credentials.
/// See also <seealso cref="ApplyFilters(string)"/>.
/// </summary>
public static void AddFilteredText(string text)
{
if (string.IsNullOrEmpty(text)) return;
lock (static_sync_lock)
filters.Add(text);
}
/// <summary>
/// Removes phrases which should be filtered from logs.
/// Useful for avoiding logging of credentials.
/// See also <seealso cref="AddFilteredText(string)"/>.
/// </summary>
public static string ApplyFilters(string message)
{
lock (static_sync_lock)
{
foreach (string f in filters)
message = message.Replace(f, string.Empty.PadRight(f.Length, '*'));
}
return message;
}
/// <summary>
/// Logs the given exception with the given description to the specified logging target.
/// </summary>
/// <param name="e">The exception that should be logged.</param>
/// <param name="description">The description of the error that should be logged with the exception.</param>
/// <param name="target">The logging target (file).</param>
/// <param name="recursive">Whether the inner exceptions of the given exception <paramref name="e"/> should be logged recursively.</param>
public static void Error(Exception e, string description, LoggingTarget target = LoggingTarget.Runtime, bool recursive = false)
{
error(e, description, target, null, recursive);
}
/// <summary>
/// Logs the given exception with the given description to the logger with the given name.
/// </summary>
/// <param name="e">The exception that should be logged.</param>
/// <param name="description">The description of the error that should be logged with the exception.</param>
/// <param name="name">The logger name (file).</param>
/// <param name="recursive">Whether the inner exceptions of the given exception <paramref name="e"/> should be logged recursively.</param>
public static void Error(Exception e, string description, string name, bool recursive = false)
{
error(e, description, null, name, recursive);
}
private static void error(Exception e, string description, LoggingTarget? target, string name, bool recursive)
{
log($@"{description}", target, name, LogLevel.Error, e);
if (recursive && e.InnerException != null)
error(e.InnerException, $"{description} (inner)", target, name, true);
}
/// <summary>
/// Log an arbitrary string to the specified logging target.
/// </summary>
/// <param name="message">The message to log. Can include newline (\n) characters to split into multiple lines.</param>
/// <param name="target">The logging target (file).</param>
/// <param name="level">The verbosity level.</param>
public static void Log(string message, LoggingTarget target = LoggingTarget.Runtime, LogLevel level = LogLevel.Verbose)
{
log(message, target, null, level);
}
/// <summary>
/// Log an arbitrary string to the logger with the given name.
/// </summary>
/// <param name="message">The message to log. Can include newline (\n) characters to split into multiple lines.</param>
/// <param name="name">The logger name (file).</param>
/// <param name="level">The verbosity level.</param>
public static void Log(string message, string name, LogLevel level = LogLevel.Verbose)
{
log(message, null, name, level);
}
private static void log(string message, LoggingTarget? target, string loggerName, LogLevel level, Exception exception = null)
{
try
{
if (target.HasValue)
GetLogger(target.Value).Add(message, level, exception);
else
GetLogger(loggerName).Add(message, level, exception);
}
catch
{
}
}
/// <summary>
/// Logs a message to the specified logging target and also displays a print statement.
/// </summary>
/// <param name="message">The message to log. Can include newline (\n) characters to split into multiple lines.</param>
/// <param name="target">The logging target (file).</param>
/// <param name="level">The verbosity level.</param>
public static void LogPrint(string message, LoggingTarget target = LoggingTarget.Runtime, LogLevel level = LogLevel.Verbose)
{
if (Enabled && DebugUtils.IsDebugBuild)
System.Diagnostics.Debug.Print(message);
Log(message, target, level);
}
/// <summary>
/// Logs a message to the logger with the given name and also displays a print statement.
/// </summary>
/// <param name="message">The message to log. Can include newline (\n) characters to split into multiple lines.</param>
/// <param name="name">The logger name (file).</param>
/// <param name="level">The verbosity level.</param>
public static void LogPrint(string message, string name, LogLevel level = LogLevel.Verbose)
{
if (Enabled && DebugUtils.IsDebugBuild)
System.Diagnostics.Debug.Print(message);
Log(message, name, level);
}
/// <summary>
/// For classes that regularly log to the same target, this method may be preferred over the static Log method.
/// </summary>
/// <param name="target">The logging target.</param>
/// <returns>The logger responsible for the given logging target.</returns>
public static Logger GetLogger(LoggingTarget target = LoggingTarget.Runtime)
{
// there can be no name conflicts between LoggingTarget-based Loggers and named loggers because
// every name that would coincide with a LoggingTarget-value is reserved and cannot be used (see ctor).
return GetLogger(target.ToString());
}
/// <summary>
/// For classes that regularly log to the same target, this method may be preferred over the static Log method.
/// </summary>
/// <param name="name">The name of the custom logger.</param>
/// <returns>The logger responsible for the given logging target.</returns>
public static Logger GetLogger(string name)
{
lock (static_sync_lock)
{
var nameLower = name.ToLower();
if (!static_loggers.TryGetValue(nameLower, out Logger l))
{
static_loggers[nameLower] = l = Enum.TryParse(name, true, out LoggingTarget target) ? new Logger(target) : new Logger(name);
l.clear();
}
return l;
}
}
/// <summary>
/// The target for which this logger logs information. This will only be null if the logger has a name.
/// </summary>
public LoggingTarget? Target { get; }
/// <summary>
/// The name of the logger. This will only have a value if <see cref="Target"/> is null.
/// </summary>
public string Name { get; }
/// <summary>
/// Gets the name of the file that this logger is logging to.
/// </summary>
public string Filename => $@"{(Target?.ToString() ?? Name).ToLower()}.log";
private Logger(LoggingTarget target = LoggingTarget.Runtime)
{
Target = target;
}
private static readonly HashSet<string> reserved_names = new HashSet<string>(Enum.GetNames(typeof(LoggingTarget)).Select(n => n.ToLower()));
private Logger(string name)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("The name of a logger must be non-null and may not contain only white space.", nameof(name));
if (reserved_names.Contains(name.ToLower()))
throw new ArgumentException($"The name \"{name}\" is reserved. Please use the {nameof(LoggingTarget)}-value corresponding to the name instead.");
Name = name;
}
/// <summary>
/// Logs a new message with the <see cref="LogLevel.Debug"/> and will only be logged if your project is built in the Debug configuration. Please note that the default setting for <see cref="Level"/> is <see cref="LogLevel.Verbose"/> so unless you increase the <see cref="Level"/> to <see cref="LogLevel.Debug"/> messages printed with this method will not appear in the output.
/// </summary>
/// <param name="message">The message that should be logged.</param>
[Conditional("DEBUG")]
public void Debug(string message = @"")
{
Add(message, LogLevel.Debug);
}
/// <summary>
/// Log an arbitrary string to current log.
/// </summary>
/// <param name="message">The message to log. Can include newline (\n) characters to split into multiple lines.</param>
/// <param name="level">The verbosity level.</param>
/// <param name="exception">An optional related exception.</param>
public void Add(string message = @"", LogLevel level = LogLevel.Verbose, Exception exception = null) =>
add(message, level, exception, OutputToListeners);
private readonly RollingTime debugOutputRollingTime = new RollingTime(50, 10000);
private void add(string message = @"", LogLevel level = LogLevel.Verbose, Exception exception = null, bool outputToListeners = true)
{
if (!Enabled || level < Level)
return;
ensureHeader();
message = ApplyFilters(message);
string logOutput = message;
if (exception != null)
// add exception output to console / logfile output (but not the LogEntry's message).
logOutput += $"\n{ApplyFilters(exception.ToString())}";
IEnumerable<string> lines = logOutput
.Replace(@"\r\n", @"\n")
.Split('\n')
.Select(s => $@"{DateTime.UtcNow.ToString(NumberFormatInfo.InvariantInfo)}: {s.Trim()}");
if (outputToListeners)
{
NewEntry?.Invoke(new LogEntry
{
Level = level,
Target = Target,
LoggerName = Name,
Message = message,
Exception = exception
});
if (DebugUtils.IsDebugBuild)
{
void consoleLog(string msg)
{
// fire to all debug listeners (like visual studio's output window)
System.Diagnostics.Debug.Print(msg);
// fire for console displays (appveyor/CI).
Console.WriteLine(msg);
}
bool bypassRateLimit = level >= LogLevel.Verbose;
foreach (var line in lines)
{
if (bypassRateLimit || debugOutputRollingTime.RequestEntry())
{
consoleLog($"[{Target?.ToString().ToLower() ?? Name}:{level.ToString().ToLower()}] {line}");
if (!bypassRateLimit && debugOutputRollingTime.IsAtLimit)
consoleLog($"Console output is being limited. Please check {Filename} for full logs.");
}
}
}
}
if (Target == LoggingTarget.Information)
// don't want to log this to a file
return;
lock (flush_sync_lock)
{
// we need to check if the logger is still enabled here, since we may have been waiting for a
// flush and while the flush was happening, the logger might have been disabled. In that case
// we want to make sure that we don't accidentally write anything to a file after that flush.
if (!Enabled)
return;
scheduler.Add(delegate
{
try
{
using (var stream = Storage.GetStream(Filename, FileAccess.Write, FileMode.Append))
using (var writer = new StreamWriter(stream))
foreach (var line in lines)
writer.WriteLine(line);
}
catch
{
}
});
writer_idle.Reset();
}
}
/// <summary>
/// Whether the output of this logger should be sent to listeners of <see cref="Debug"/> and <see cref="Console"/>.
/// Defaults to true.
/// </summary>
public bool OutputToListeners { get; set; } = true;
/// <summary>
/// Fires whenever any logger tries to log a new entry, but before the entry is actually written to the logfile.
/// </summary>
public static event Action<LogEntry> NewEntry;
/// <summary>
/// Deletes log file from disk.
/// </summary>
private void clear()
{
lock (flush_sync_lock)
{
scheduler.Add(() => Storage.Delete(Filename));
writer_idle.Reset();
}
}
private bool headerAdded;
private void ensureHeader()
{
if (headerAdded) return;
headerAdded = true;
add("----------------------------------------------------------", outputToListeners: false);
add($"{Target} Log for {UserIdentifier} (LogLevel: {Level})", outputToListeners: false);
add($"{GameIdentifier} {VersionIdentifier}", outputToListeners: false);
add($"Running on {Environment.OSVersion}, {Environment.ProcessorCount} cores", outputToListeners: false);
add("----------------------------------------------------------", outputToListeners: false);
}
private static readonly List<string> filters = new List<string>();
private static readonly Dictionary<string, Logger> static_loggers = new Dictionary<string, Logger>();
private static readonly Scheduler scheduler = new Scheduler();
private static readonly ManualResetEvent writer_idle = new ManualResetEvent(true);
private static readonly Timer timer;
static Logger()
{
// timer has a very low overhead.
timer = new Timer(_ =>
{
if ((Storage != null ? scheduler.Update() : 0) == 0)
writer_idle.Set();
// reschedule every 50ms. avoids overlapping callbacks.
timer.Change(50, Timeout.Infinite);
}, null, 0, Timeout.Infinite);
}
/// <summary>
/// Pause execution until all logger writes have completed and file handles have been closed.
/// This will also unbind all handlers bound to <see cref="NewEntry"/>.
/// </summary>
public static void Flush()
{
lock (flush_sync_lock)
{
writer_idle.WaitOne(500);
NewEntry = null;
}
}
}
/// <summary>
/// Captures information about a logged message.
/// </summary>
public class LogEntry
{
/// <summary>
/// The level for which the message was logged.
/// </summary>
public LogLevel Level;
/// <summary>
/// The target to which this message is being logged, or null if it is being logged to a custom named logger.
/// </summary>
public LoggingTarget? Target;
/// <summary>
/// The name of the logger to which this message is being logged, or null if it is being logged to a specific <see cref="LoggingTarget"/>.
/// </summary>
public string LoggerName;
/// <summary>
/// The message that was logged.
/// </summary>
public string Message;
/// <summary>
/// An optional related exception.
/// </summary>
public Exception Exception;
}
/// <summary>
/// The level on which a log-message is logged.
/// </summary>
public enum LogLevel
{
/// <summary>
/// Log-level for debugging-related log-messages. This is the lowest level (highest verbosity). Please note that this will log input events, including keypresses when entering a password.
/// </summary>
Debug,
/// <summary>
/// Log-level for most log-messages. This is the second-lowest level (second-highest verbosity).
/// </summary>
Verbose,
/// <summary>
/// Log-level for important log-messages. This is the second-highest level (second-lowest verbosity).
/// </summary>
Important,
/// <summary>
/// Log-level for error messages. This is the highest level (lowest verbosity).
/// </summary>
Error
}
/// <summary>
/// The target for logging. Different targets can have different logfiles, are displayed differently in the LogOverlay and are generally useful for organizing logs into groups.
/// </summary>
public enum LoggingTarget
{
/// <summary>
/// Logging target for general information. Everything logged with this target will not be written to a logfile.
/// </summary>
Information,
/// <summary>
/// Logging target for information about the runtime.
/// </summary>
Runtime,
/// <summary>
/// Logging target for network-related events.
/// </summary>
Network,
/// <summary>
/// Logging target for performance-related information.
/// </summary>
Performance,
/// <summary>
/// Logging target for database-related events.
/// </summary>
Database
}
}