diff options
Diffstat (limited to 'src/Ryujinx.Common/Logging')
| -rw-r--r-- | src/Ryujinx.Common/Logging/Formatters/DefaultLogFormatter.cs | 42 | ||||
| -rw-r--r-- | src/Ryujinx.Common/Logging/Formatters/DynamicObjectFormatter.cs | 84 | ||||
| -rw-r--r-- | src/Ryujinx.Common/Logging/Formatters/ILogFormatter.cs | 7 | ||||
| -rw-r--r-- | src/Ryujinx.Common/Logging/LogClass.cs | 76 | ||||
| -rw-r--r-- | src/Ryujinx.Common/Logging/LogEventArgs.cs | 23 | ||||
| -rw-r--r-- | src/Ryujinx.Common/Logging/LogEventArgsJson.cs | 30 | ||||
| -rw-r--r-- | src/Ryujinx.Common/Logging/LogEventJsonSerializerContext.cs | 9 | ||||
| -rw-r--r-- | src/Ryujinx.Common/Logging/LogLevel.cs | 19 | ||||
| -rw-r--r-- | src/Ryujinx.Common/Logging/Logger.cs | 224 | ||||
| -rw-r--r-- | src/Ryujinx.Common/Logging/Targets/AsyncLogTargetWrapper.cs | 79 | ||||
| -rw-r--r-- | src/Ryujinx.Common/Logging/Targets/ConsoleLogTarget.cs | 41 | ||||
| -rw-r--r-- | src/Ryujinx.Common/Logging/Targets/FileLogTarget.cs | 55 | ||||
| -rw-r--r-- | src/Ryujinx.Common/Logging/Targets/ILogTarget.cs | 11 | ||||
| -rw-r--r-- | src/Ryujinx.Common/Logging/Targets/JsonLogTarget.cs | 40 |
14 files changed, 740 insertions, 0 deletions
diff --git a/src/Ryujinx.Common/Logging/Formatters/DefaultLogFormatter.cs b/src/Ryujinx.Common/Logging/Formatters/DefaultLogFormatter.cs new file mode 100644 index 00000000..28a7d546 --- /dev/null +++ b/src/Ryujinx.Common/Logging/Formatters/DefaultLogFormatter.cs @@ -0,0 +1,42 @@ +using System.Text; + +namespace Ryujinx.Common.Logging +{ + internal class DefaultLogFormatter : ILogFormatter + { + private static readonly ObjectPool<StringBuilder> StringBuilderPool = SharedPools.Default<StringBuilder>(); + + public string Format(LogEventArgs args) + { + StringBuilder sb = StringBuilderPool.Allocate(); + + try + { + sb.Clear(); + + sb.Append($@"{args.Time:hh\:mm\:ss\.fff}"); + sb.Append($" |{args.Level.ToString()[0]}| "); + + if (args.ThreadName != null) + { + sb.Append(args.ThreadName); + sb.Append(' '); + } + + sb.Append(args.Message); + + if (args.Data is not null) + { + sb.Append(' '); + DynamicObjectFormatter.Format(sb, args.Data); + } + + return sb.ToString(); + } + finally + { + StringBuilderPool.Release(sb); + } + } + } +} diff --git a/src/Ryujinx.Common/Logging/Formatters/DynamicObjectFormatter.cs b/src/Ryujinx.Common/Logging/Formatters/DynamicObjectFormatter.cs new file mode 100644 index 00000000..5f15cc2a --- /dev/null +++ b/src/Ryujinx.Common/Logging/Formatters/DynamicObjectFormatter.cs @@ -0,0 +1,84 @@ +#nullable enable +using System; +using System.Reflection; +using System.Text; + +namespace Ryujinx.Common.Logging +{ + internal class DynamicObjectFormatter + { + private static readonly ObjectPool<StringBuilder> StringBuilderPool = SharedPools.Default<StringBuilder>(); + + public static string? Format(object? dynamicObject) + { + if (dynamicObject is null) + { + return null; + } + + StringBuilder sb = StringBuilderPool.Allocate(); + + try + { + Format(sb, dynamicObject); + + return sb.ToString(); + } + finally + { + StringBuilderPool.Release(sb); + } + } + + public static void Format(StringBuilder sb, object? dynamicObject) + { + if (dynamicObject is null) + { + return; + } + + PropertyInfo[] props = dynamicObject.GetType().GetProperties(); + + sb.Append('{'); + + foreach (var prop in props) + { + sb.Append(prop.Name); + sb.Append(": "); + + if (typeof(Array).IsAssignableFrom(prop.PropertyType)) + { + Array? array = (Array?) prop.GetValue(dynamicObject); + + if (array is not null) + { + foreach (var item in array) + { + sb.Append(item); + sb.Append(", "); + } + + if (array.Length > 0) + { + sb.Remove(sb.Length - 2, 2); + } + } + } + else + { + sb.Append(prop.GetValue(dynamicObject)); + } + + sb.Append(" ; "); + } + + // We remove the final ';' from the string + if (props.Length > 0) + { + sb.Remove(sb.Length - 3, 3); + } + + sb.Append('}'); + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Common/Logging/Formatters/ILogFormatter.cs b/src/Ryujinx.Common/Logging/Formatters/ILogFormatter.cs new file mode 100644 index 00000000..9a55bc6b --- /dev/null +++ b/src/Ryujinx.Common/Logging/Formatters/ILogFormatter.cs @@ -0,0 +1,7 @@ +namespace Ryujinx.Common.Logging +{ + interface ILogFormatter + { + string Format(LogEventArgs args); + } +} diff --git a/src/Ryujinx.Common/Logging/LogClass.cs b/src/Ryujinx.Common/Logging/LogClass.cs new file mode 100644 index 00000000..e62676cd --- /dev/null +++ b/src/Ryujinx.Common/Logging/LogClass.cs @@ -0,0 +1,76 @@ +using Ryujinx.Common.Utilities; +using System.Text.Json.Serialization; + +namespace Ryujinx.Common.Logging +{ + [JsonConverter(typeof(TypedStringEnumConverter<LogClass>))] + public enum LogClass + { + Application, + Audio, + AudioRenderer, + Configuration, + Cpu, + Emulation, + FFmpeg, + Font, + Gpu, + Hid, + Host1x, + Kernel, + KernelIpc, + KernelScheduler, + KernelSvc, + Loader, + ModLoader, + Nvdec, + Ptc, + Service, + ServiceAcc, + ServiceAm, + ServiceApm, + ServiceAudio, + ServiceBcat, + ServiceBsd, + ServiceBtm, + ServiceCaps, + ServiceFatal, + ServiceFriend, + ServiceFs, + ServiceHid, + ServiceIrs, + ServiceLdn, + ServiceLdr, + ServiceLm, + ServiceMii, + ServiceMm, + ServiceMnpp, + ServiceNfc, + ServiceNfp, + ServiceNgct, + ServiceNifm, + ServiceNim, + ServiceNs, + ServiceNsd, + ServiceNtc, + ServiceNv, + ServiceOlsc, + ServicePctl, + ServicePcv, + ServicePl, + ServicePrepo, + ServicePsm, + ServicePtm, + ServiceSet, + ServiceSfdnsres, + ServiceSm, + ServiceSsl, + ServiceSss, + ServiceTime, + ServiceVi, + SurfaceFlinger, + TamperMachine, + Ui, + Vic + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Common/Logging/LogEventArgs.cs b/src/Ryujinx.Common/Logging/LogEventArgs.cs new file mode 100644 index 00000000..a27af780 --- /dev/null +++ b/src/Ryujinx.Common/Logging/LogEventArgs.cs @@ -0,0 +1,23 @@ +using System; + +namespace Ryujinx.Common.Logging +{ + public class LogEventArgs : EventArgs + { + public readonly LogLevel Level; + public readonly TimeSpan Time; + public readonly string ThreadName; + + public readonly string Message; + public readonly object Data; + + public LogEventArgs(LogLevel level, TimeSpan time, string threadName, string message, object data = null) + { + Level = level; + Time = time; + ThreadName = threadName; + Message = message; + Data = data; + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Common/Logging/LogEventArgsJson.cs b/src/Ryujinx.Common/Logging/LogEventArgsJson.cs new file mode 100644 index 00000000..425b9766 --- /dev/null +++ b/src/Ryujinx.Common/Logging/LogEventArgsJson.cs @@ -0,0 +1,30 @@ +using System; +using System.Text.Json.Serialization; + +namespace Ryujinx.Common.Logging +{ + internal class LogEventArgsJson + { + public LogLevel Level { get; } + public TimeSpan Time { get; } + public string ThreadName { get; } + + public string Message { get; } + public string Data { get; } + + [JsonConstructor] + public LogEventArgsJson(LogLevel level, TimeSpan time, string threadName, string message, string data = null) + { + Level = level; + Time = time; + ThreadName = threadName; + Message = message; + Data = data; + } + + public static LogEventArgsJson FromLogEventArgs(LogEventArgs args) + { + return new LogEventArgsJson(args.Level, args.Time, args.ThreadName, args.Message, DynamicObjectFormatter.Format(args.Data)); + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Common/Logging/LogEventJsonSerializerContext.cs b/src/Ryujinx.Common/Logging/LogEventJsonSerializerContext.cs new file mode 100644 index 00000000..da21f11e --- /dev/null +++ b/src/Ryujinx.Common/Logging/LogEventJsonSerializerContext.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Ryujinx.Common.Logging +{ + [JsonSerializable(typeof(LogEventArgsJson))] + internal partial class LogEventJsonSerializerContext : JsonSerializerContext + { + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Common/Logging/LogLevel.cs b/src/Ryujinx.Common/Logging/LogLevel.cs new file mode 100644 index 00000000..3786c756 --- /dev/null +++ b/src/Ryujinx.Common/Logging/LogLevel.cs @@ -0,0 +1,19 @@ +using Ryujinx.Common.Utilities; +using System.Text.Json.Serialization; + +namespace Ryujinx.Common.Logging +{ + [JsonConverter(typeof(TypedStringEnumConverter<LogLevel>))] + public enum LogLevel + { + Debug, + Stub, + Info, + Warning, + Error, + Guest, + AccessLog, + Notice, + Trace + } +} diff --git a/src/Ryujinx.Common/Logging/Logger.cs b/src/Ryujinx.Common/Logging/Logger.cs new file mode 100644 index 00000000..4d48dd48 --- /dev/null +++ b/src/Ryujinx.Common/Logging/Logger.cs @@ -0,0 +1,224 @@ +using Ryujinx.Common.SystemInterop; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace Ryujinx.Common.Logging +{ + public static class Logger + { + private static readonly Stopwatch m_Time; + + private static readonly bool[] m_EnabledClasses; + + private static readonly List<ILogTarget> m_LogTargets; + + private static readonly StdErrAdapter _stdErrAdapter; + + public static event EventHandler<LogEventArgs> Updated; + + public readonly struct Log + { + internal readonly LogLevel Level; + + internal Log(LogLevel level) + { + Level = level; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void PrintMsg(LogClass logClass, string message) + { + if (m_EnabledClasses[(int)logClass]) + { + Updated?.Invoke(null, new LogEventArgs(Level, m_Time.Elapsed, Thread.CurrentThread.Name, FormatMessage(logClass, "", message))); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Print(LogClass logClass, string message, [CallerMemberName] string caller = "") + { + if (m_EnabledClasses[(int)logClass]) + { + Updated?.Invoke(null, new LogEventArgs(Level, m_Time.Elapsed, Thread.CurrentThread.Name, FormatMessage(logClass, caller, message))); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Print(LogClass logClass, string message, object data, [CallerMemberName] string caller = "") + { + if (m_EnabledClasses[(int)logClass]) + { + Updated?.Invoke(null, new LogEventArgs(Level, m_Time.Elapsed, Thread.CurrentThread.Name, FormatMessage(logClass, caller, message), data)); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void PrintStub(LogClass logClass, string message = "", [CallerMemberName] string caller = "") + { + if (m_EnabledClasses[(int)logClass]) + { + Updated?.Invoke(null, new LogEventArgs(Level, m_Time.Elapsed, Thread.CurrentThread.Name, FormatMessage(logClass, caller, "Stubbed. " + message))); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void PrintStub(LogClass logClass, object data, [CallerMemberName] string caller = "") + { + if (m_EnabledClasses[(int)logClass]) + { + Updated?.Invoke(null, new LogEventArgs(Level, m_Time.Elapsed, Thread.CurrentThread.Name, FormatMessage(logClass, caller, "Stubbed."), data)); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void PrintStub(LogClass logClass, string message, object data, [CallerMemberName] string caller = "") + { + if (m_EnabledClasses[(int)logClass]) + { + Updated?.Invoke(null, new LogEventArgs(Level, m_Time.Elapsed, Thread.CurrentThread.Name, FormatMessage(logClass, caller, "Stubbed. " + message), data)); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void PrintRawMsg(string message) + { + Updated?.Invoke(null, new LogEventArgs(Level, m_Time.Elapsed, Thread.CurrentThread.Name, message)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static string FormatMessage(LogClass Class, string Caller, string Message) => $"{Class} {Caller}: {Message}"; + } + + public static Log? Debug { get; private set; } + public static Log? Info { get; private set; } + public static Log? Warning { get; private set; } + public static Log? Error { get; private set; } + public static Log? Guest { get; private set; } + public static Log? AccessLog { get; private set; } + public static Log? Stub { get; private set; } + public static Log? Trace { get; private set; } + public static Log Notice { get; } // Always enabled + + static Logger() + { + m_EnabledClasses = new bool[Enum.GetNames<LogClass>().Length]; + + for (int index = 0; index < m_EnabledClasses.Length; index++) + { + m_EnabledClasses[index] = true; + } + + m_LogTargets = new List<ILogTarget>(); + + m_Time = Stopwatch.StartNew(); + + // Logger should log to console by default + AddTarget(new AsyncLogTargetWrapper( + new ConsoleLogTarget("console"), + 1000, + AsyncLogTargetOverflowAction.Discard)); + + Notice = new Log(LogLevel.Notice); + + // Enable important log levels before configuration is loaded + Error = new Log(LogLevel.Error); + Warning = new Log(LogLevel.Warning); + Info = new Log(LogLevel.Info); + Trace = new Log(LogLevel.Trace); + + _stdErrAdapter = new StdErrAdapter(); + } + + public static void RestartTime() + { + m_Time.Restart(); + } + + private static ILogTarget GetTarget(string targetName) + { + foreach (var target in m_LogTargets) + { + if (target.Name.Equals(targetName)) + { + return target; + } + } + + return null; + } + + public static void AddTarget(ILogTarget target) + { + m_LogTargets.Add(target); + + Updated += target.Log; + } + + public static void RemoveTarget(string target) + { + ILogTarget logTarget = GetTarget(target); + + if (logTarget != null) + { + Updated -= logTarget.Log; + + m_LogTargets.Remove(logTarget); + + logTarget.Dispose(); + } + } + + public static void Shutdown() + { + Updated = null; + + _stdErrAdapter.Dispose(); + + foreach (var target in m_LogTargets) + { + target.Dispose(); + } + + m_LogTargets.Clear(); + } + + public static IReadOnlyCollection<LogLevel> GetEnabledLevels() + { + var logs = new Log?[] { Debug, Info, Warning, Error, Guest, AccessLog, Stub, Trace }; + List<LogLevel> levels = new List<LogLevel>(logs.Length); + foreach (var log in logs) + { + if (log.HasValue) + { + levels.Add(log.Value.Level); + } + } + + return levels; + } + + public static void SetEnable(LogLevel logLevel, bool enabled) + { + switch (logLevel) + { + case LogLevel.Debug : Debug = enabled ? new Log(LogLevel.Debug) : new Log?(); break; + case LogLevel.Info : Info = enabled ? new Log(LogLevel.Info) : new Log?(); break; + case LogLevel.Warning : Warning = enabled ? new Log(LogLevel.Warning) : new Log?(); break; + case LogLevel.Error : Error = enabled ? new Log(LogLevel.Error) : new Log?(); break; + case LogLevel.Guest : Guest = enabled ? new Log(LogLevel.Guest) : new Log?(); break; + case LogLevel.AccessLog : AccessLog = enabled ? new Log(LogLevel.AccessLog): new Log?(); break; + case LogLevel.Stub : Stub = enabled ? new Log(LogLevel.Stub) : new Log?(); break; + case LogLevel.Trace : Trace = enabled ? new Log(LogLevel.Trace) : new Log?(); break; + default: throw new ArgumentException("Unknown Log Level"); + } + } + + public static void SetEnable(LogClass logClass, bool enabled) + { + m_EnabledClasses[(int)logClass] = enabled; + } + } +} diff --git a/src/Ryujinx.Common/Logging/Targets/AsyncLogTargetWrapper.cs b/src/Ryujinx.Common/Logging/Targets/AsyncLogTargetWrapper.cs new file mode 100644 index 00000000..43c62d31 --- /dev/null +++ b/src/Ryujinx.Common/Logging/Targets/AsyncLogTargetWrapper.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Concurrent; +using System.Threading; + +namespace Ryujinx.Common.Logging +{ + public enum AsyncLogTargetOverflowAction + { + /// <summary> + /// Block until there's more room in the queue + /// </summary> + Block = 0, + + /// <summary> + /// Discard the overflowing item + /// </summary> + Discard = 1 + } + + public class AsyncLogTargetWrapper : ILogTarget + { + private ILogTarget _target; + + private Thread _messageThread; + + private BlockingCollection<LogEventArgs> _messageQueue; + + private readonly int _overflowTimeout; + + string ILogTarget.Name { get => _target.Name; } + + public AsyncLogTargetWrapper(ILogTarget target) + : this(target, -1, AsyncLogTargetOverflowAction.Block) + { } + + public AsyncLogTargetWrapper(ILogTarget target, int queueLimit, AsyncLogTargetOverflowAction overflowAction) + { + _target = target; + _messageQueue = new BlockingCollection<LogEventArgs>(queueLimit); + _overflowTimeout = overflowAction == AsyncLogTargetOverflowAction.Block ? -1 : 0; + + _messageThread = new Thread(() => { + while (!_messageQueue.IsCompleted) + { + try + { + _target.Log(this, _messageQueue.Take()); + } + catch (InvalidOperationException) + { + // IOE means that Take() was called on a completed collection. + // Some other thread can call CompleteAdding after we pass the + // IsCompleted check but before we call Take. + // We can simply catch the exception since the loop will break + // on the next iteration. + } + } + }); + + _messageThread.Name = "Logger.MessageThread"; + _messageThread.IsBackground = true; + _messageThread.Start(); + } + + public void Log(object sender, LogEventArgs e) + { + if (!_messageQueue.IsAddingCompleted) + { + _messageQueue.TryAdd(e, _overflowTimeout); + } + } + + public void Dispose() + { + _messageQueue.CompleteAdding(); + _messageThread.Join(); + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Common/Logging/Targets/ConsoleLogTarget.cs b/src/Ryujinx.Common/Logging/Targets/ConsoleLogTarget.cs new file mode 100644 index 00000000..7b77c4f2 --- /dev/null +++ b/src/Ryujinx.Common/Logging/Targets/ConsoleLogTarget.cs @@ -0,0 +1,41 @@ +using System; + +namespace Ryujinx.Common.Logging +{ + public class ConsoleLogTarget : ILogTarget + { + private readonly ILogFormatter _formatter; + + private readonly string _name; + + string ILogTarget.Name { get => _name; } + + private static ConsoleColor GetLogColor(LogLevel level) => level switch { + LogLevel.Info => ConsoleColor.White, + LogLevel.Warning => ConsoleColor.Yellow, + LogLevel.Error => ConsoleColor.Red, + LogLevel.Stub => ConsoleColor.DarkGray, + LogLevel.Notice => ConsoleColor.Cyan, + LogLevel.Trace => ConsoleColor.DarkCyan, + _ => ConsoleColor.Gray, + }; + + public ConsoleLogTarget(string name) + { + _formatter = new DefaultLogFormatter(); + _name = name; + } + + public void Log(object sender, LogEventArgs args) + { + Console.ForegroundColor = GetLogColor(args.Level); + Console.WriteLine(_formatter.Format(args)); + Console.ResetColor(); + } + + public void Dispose() + { + Console.ResetColor(); + } + } +} diff --git a/src/Ryujinx.Common/Logging/Targets/FileLogTarget.cs b/src/Ryujinx.Common/Logging/Targets/FileLogTarget.cs new file mode 100644 index 00000000..24dd6d17 --- /dev/null +++ b/src/Ryujinx.Common/Logging/Targets/FileLogTarget.cs @@ -0,0 +1,55 @@ +using System; +using System.IO; +using System.Linq; + +namespace Ryujinx.Common.Logging +{ + public class FileLogTarget : ILogTarget + { + private readonly StreamWriter _logWriter; + private readonly ILogFormatter _formatter; + private readonly string _name; + + string ILogTarget.Name { get => _name; } + + public FileLogTarget(string path, string name) + : this(path, name, FileShare.Read, FileMode.Append) + { } + + public FileLogTarget(string path, string name, FileShare fileShare, FileMode fileMode) + { + // Ensure directory is present + DirectoryInfo logDir = new DirectoryInfo(Path.Combine(path, "Logs")); + logDir.Create(); + + // Clean up old logs, should only keep 3 + FileInfo[] files = logDir.GetFiles("*.log").OrderBy((info => info.CreationTime)).ToArray(); + for (int i = 0; i < files.Length - 2; i++) + { + files[i].Delete(); + } + + string version = ReleaseInformation.GetVersion(); + + // Get path for the current time + path = Path.Combine(logDir.FullName, $"Ryujinx_{version}_{DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss")}.log"); + + _name = name; + _logWriter = new StreamWriter(File.Open(path, fileMode, FileAccess.Write, fileShare)); + _formatter = new DefaultLogFormatter(); + } + + public void Log(object sender, LogEventArgs args) + { + _logWriter.WriteLine(_formatter.Format(args)); + _logWriter.Flush(); + } + + public void Dispose() + { + _logWriter.WriteLine("---- End of Log ----"); + _logWriter.Flush(); + _logWriter.Dispose(); + } + } +} diff --git a/src/Ryujinx.Common/Logging/Targets/ILogTarget.cs b/src/Ryujinx.Common/Logging/Targets/ILogTarget.cs new file mode 100644 index 00000000..d4d26a93 --- /dev/null +++ b/src/Ryujinx.Common/Logging/Targets/ILogTarget.cs @@ -0,0 +1,11 @@ +using System; + +namespace Ryujinx.Common.Logging +{ + public interface ILogTarget : IDisposable + { + void Log(object sender, LogEventArgs args); + + string Name { get; } + } +} diff --git a/src/Ryujinx.Common/Logging/Targets/JsonLogTarget.cs b/src/Ryujinx.Common/Logging/Targets/JsonLogTarget.cs new file mode 100644 index 00000000..06976433 --- /dev/null +++ b/src/Ryujinx.Common/Logging/Targets/JsonLogTarget.cs @@ -0,0 +1,40 @@ +using Ryujinx.Common.Utilities; +using System.IO; + +namespace Ryujinx.Common.Logging +{ + public class JsonLogTarget : ILogTarget + { + private Stream _stream; + private bool _leaveOpen; + private string _name; + + string ILogTarget.Name { get => _name; } + + public JsonLogTarget(Stream stream, string name) + { + _stream = stream; + _name = name; + } + + public JsonLogTarget(Stream stream, bool leaveOpen) + { + _stream = stream; + _leaveOpen = leaveOpen; + } + + public void Log(object sender, LogEventArgs e) + { + var logEventArgsJson = LogEventArgsJson.FromLogEventArgs(e); + JsonHelper.SerializeToStream(_stream, logEventArgsJson, LogEventJsonSerializerContext.Default.LogEventArgsJson); + } + + public void Dispose() + { + if (!_leaveOpen) + { + _stream.Dispose(); + } + } + } +} |
