From 50e7028aa8428c58a4adb3962b2325573433dc39 Mon Sep 17 00:00:00 2001 From: Ionite Date: Mon, 28 Aug 2023 18:29:52 -0400 Subject: [PATCH] Add LogViewer --- .../LogViewer/Controls/LogViewerControl.axaml | 74 ++++++++++++++++++ .../Controls/LogViewerControl.axaml.cs | 52 +++++++++++++ .../Converters/ChangeColorTypeConverter.cs | 25 +++++++ .../LogViewer/Converters/EventIdConverter.cs | 22 ++++++ .../Core/Extensions/LoggerExtensions.cs | 55 ++++++++++++++ .../Logging/DataStoreLoggerConfiguration.cs | 47 ++++++++++++ .../LogViewer/Core/Logging/ILogDataStore.cs | 9 +++ .../Core/Logging/ILogDataStoreImpl.cs | 6 ++ .../LogViewer/Core/Logging/LogDataStore.cs | 38 ++++++++++ .../LogViewer/Core/Logging/LogEntryColor.cs | 20 +++++ .../LogViewer/Core/Logging/LogModel.cs | 33 ++++++++ .../ViewModels/LogViewerControlViewModel.cs | 21 ++++++ .../Core/ViewModels/ObservableObject.cs | 21 ++++++ .../LogViewer/Core/ViewModels/ViewModel.cs | 3 + .../LogViewer/DataStoreLoggerTarget.cs | 75 +++++++++++++++++++ .../LogViewer/Extensions/ServicesExtension.cs | 65 ++++++++++++++++ .../LogViewer/LICENSE | 21 ++++++ .../LogViewer/Logging/LogDataStore.cs | 13 ++++ .../LogViewer/README.md | 3 + ...tabilityMatrix.Avalonia.Diagnostics.csproj | 36 +++++++++ .../ViewModels/LogWindowViewModel.cs | 20 +++++ .../Views/LogWindow.axaml | 22 ++++++ .../Views/LogWindow.axaml.cs | 39 ++++++++++ StabilityMatrix.Avalonia/App.axaml.cs | 43 ++++++++++- .../StabilityMatrix.Avalonia.csproj | 8 +- .../Views/LaunchPageView.axaml.cs | 6 -- .../Views/MainWindow.axaml.cs | 2 + StabilityMatrix.sln | 6 ++ 28 files changed, 776 insertions(+), 9 deletions(-) create mode 100644 StabilityMatrix.Avalonia.Diagnostics/LogViewer/Controls/LogViewerControl.axaml create mode 100644 StabilityMatrix.Avalonia.Diagnostics/LogViewer/Controls/LogViewerControl.axaml.cs create mode 100644 StabilityMatrix.Avalonia.Diagnostics/LogViewer/Converters/ChangeColorTypeConverter.cs create mode 100644 StabilityMatrix.Avalonia.Diagnostics/LogViewer/Converters/EventIdConverter.cs create mode 100644 StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Extensions/LoggerExtensions.cs create mode 100644 StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/DataStoreLoggerConfiguration.cs create mode 100644 StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/ILogDataStore.cs create mode 100644 StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/ILogDataStoreImpl.cs create mode 100644 StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/LogDataStore.cs create mode 100644 StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/LogEntryColor.cs create mode 100644 StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/LogModel.cs create mode 100644 StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/ViewModels/LogViewerControlViewModel.cs create mode 100644 StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/ViewModels/ObservableObject.cs create mode 100644 StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/ViewModels/ViewModel.cs create mode 100644 StabilityMatrix.Avalonia.Diagnostics/LogViewer/DataStoreLoggerTarget.cs create mode 100644 StabilityMatrix.Avalonia.Diagnostics/LogViewer/Extensions/ServicesExtension.cs create mode 100644 StabilityMatrix.Avalonia.Diagnostics/LogViewer/LICENSE create mode 100644 StabilityMatrix.Avalonia.Diagnostics/LogViewer/Logging/LogDataStore.cs create mode 100644 StabilityMatrix.Avalonia.Diagnostics/LogViewer/README.md create mode 100644 StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj create mode 100644 StabilityMatrix.Avalonia.Diagnostics/ViewModels/LogWindowViewModel.cs create mode 100644 StabilityMatrix.Avalonia.Diagnostics/Views/LogWindow.axaml create mode 100644 StabilityMatrix.Avalonia.Diagnostics/Views/LogWindow.axaml.cs diff --git a/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Controls/LogViewerControl.axaml b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Controls/LogViewerControl.axaml new file mode 100644 index 00000000..f8d3a9a6 --- /dev/null +++ b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Controls/LogViewerControl.axaml @@ -0,0 +1,74 @@ + + + + + + + Black + Transparent + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Controls/LogViewerControl.axaml.cs b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Controls/LogViewerControl.axaml.cs new file mode 100644 index 00000000..4cd3e8b4 --- /dev/null +++ b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Controls/LogViewerControl.axaml.cs @@ -0,0 +1,52 @@ +using System.Collections.Specialized; +using Avalonia.Controls; +using Avalonia.LogicalTree; +using Avalonia.Threading; +using StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.Logging; + +namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Controls; + +public partial class LogViewerControl : UserControl +{ + public LogViewerControl() + => InitializeComponent(); + + private ILogDataStoreImpl? vm; + private LogModel? item; + + protected override void OnDataContextChanged(EventArgs e) + { + base.OnDataContextChanged(e); + + if (DataContext is null) + return; + + vm = (ILogDataStoreImpl)DataContext; + vm.DataStore.Entries.CollectionChanged += OnCollectionChanged; + } + + private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + Dispatcher.UIThread.Post(() => + { + item = MyDataGrid.ItemsSource.Cast().LastOrDefault(); + }); + } + + protected void OnLayoutUpdated(object? sender, EventArgs e) + { + if (CanAutoScroll.IsChecked != true || item is null) + return; + + MyDataGrid.ScrollIntoView(item, null); + item = null; + } + + protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) + { + base.OnDetachedFromLogicalTree(e); + + if (vm is null) return; + vm.DataStore.Entries.CollectionChanged -= OnCollectionChanged; + } +} diff --git a/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Converters/ChangeColorTypeConverter.cs b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Converters/ChangeColorTypeConverter.cs new file mode 100644 index 00000000..9816aa25 --- /dev/null +++ b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Converters/ChangeColorTypeConverter.cs @@ -0,0 +1,25 @@ +using System.Globalization; +using Avalonia.Data.Converters; +using Avalonia.Media; +using SysDrawColor = System.Drawing.Color; + +namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Converters; + +public class ChangeColorTypeConverter : IValueConverter +{ + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is null) + return new SolidColorBrush((Color)(parameter ?? Colors.Black)); + + var sysDrawColor = (SysDrawColor)value!; + return new SolidColorBrush(Color.FromArgb( + sysDrawColor.A, + sysDrawColor.R, + sysDrawColor.G, + sysDrawColor.B)); + } + + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + => throw new NotImplementedException(); +} diff --git a/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Converters/EventIdConverter.cs b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Converters/EventIdConverter.cs new file mode 100644 index 00000000..081ce049 --- /dev/null +++ b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Converters/EventIdConverter.cs @@ -0,0 +1,22 @@ +using System.Globalization; +using Avalonia.Data.Converters; +using Microsoft.Extensions.Logging; + +namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Converters; + +public class EventIdConverter : IValueConverter +{ + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is null) + return "0"; + + var eventId = (EventId)value; + + return eventId.ToString(); + } + + // If not implemented, an error is thrown + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + => new EventId(0, value?.ToString() ?? string.Empty); +} diff --git a/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Extensions/LoggerExtensions.cs b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Extensions/LoggerExtensions.cs new file mode 100644 index 00000000..7c84fa05 --- /dev/null +++ b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Extensions/LoggerExtensions.cs @@ -0,0 +1,55 @@ +using Microsoft.Extensions.Logging; + +namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.Extensions; + +public static class LoggerExtensions +{ + public static void Emit(this ILogger logger, EventId eventId, + LogLevel logLevel, string message, Exception? exception = null, params object?[] args) + { + if (logger is null) + return; + + //if (!logger.IsEnabled(logLevel)) + // return; + + switch (logLevel) + { + case LogLevel.Trace: + logger.LogTrace(eventId, message, args); + break; + + case LogLevel.Debug: + logger.LogDebug(eventId, message, args); + break; + + case LogLevel.Information: + logger.LogInformation(eventId, message, args); + break; + + case LogLevel.Warning: + logger.LogWarning(eventId, exception, message, args); + break; + + case LogLevel.Error: + logger.LogError(eventId, exception, message, args); + break; + + case LogLevel.Critical: + logger.LogCritical(eventId, exception, message, args); + break; + } + } + + public static void TestPattern(this ILogger logger, EventId eventId) + { + var exception = new Exception("Test Error Message"); + + logger.Emit(eventId, LogLevel.Trace, "Trace Test Pattern"); + logger.Emit(eventId, LogLevel.Debug, "Debug Test Pattern"); + logger.Emit(eventId, LogLevel.Information, "Information Test Pattern"); + logger.Emit(eventId, LogLevel.Warning, "Warning Test Pattern"); + logger.Emit(eventId, LogLevel.Error, "Error Test Pattern", exception); + logger.Emit(eventId, LogLevel.Critical, "Critical Test Pattern", exception); + } +} diff --git a/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/DataStoreLoggerConfiguration.cs b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/DataStoreLoggerConfiguration.cs new file mode 100644 index 00000000..35d93f0c --- /dev/null +++ b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/DataStoreLoggerConfiguration.cs @@ -0,0 +1,47 @@ +using System.Drawing; +using Microsoft.Extensions.Logging; + +namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.Logging; + +public class DataStoreLoggerConfiguration +{ + #region Properties + + public EventId EventId { get; set; } + + public Dictionary Colors { get; } = new() + { + [LogLevel.Trace] = new LogEntryColor + { + Foreground = Color.DarkGray + }, + [LogLevel.Debug] = new LogEntryColor + { + Foreground = Color.Gray + }, + [LogLevel.Information] = new LogEntryColor + { + Foreground = Color.WhiteSmoke, + }, + [LogLevel.Warning] = new LogEntryColor + { + Foreground = Color.Orange + }, + [LogLevel.Error] = new LogEntryColor + { + Foreground = Color.White, + Background = Color.OrangeRed + }, + [LogLevel.Critical] = new LogEntryColor + { + Foreground = Color.White, + Background = Color.Red + }, + [LogLevel.None] = new LogEntryColor + { + Foreground = Color.Magenta + } + }; + + #endregion +} diff --git a/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/ILogDataStore.cs b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/ILogDataStore.cs new file mode 100644 index 00000000..caee277f --- /dev/null +++ b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/ILogDataStore.cs @@ -0,0 +1,9 @@ +using System.Collections.ObjectModel; + +namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.Logging; + +public interface ILogDataStore +{ + ObservableCollection Entries { get; } + void AddEntry(LogModel logModel); +} diff --git a/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/ILogDataStoreImpl.cs b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/ILogDataStoreImpl.cs new file mode 100644 index 00000000..1f3f3811 --- /dev/null +++ b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/ILogDataStoreImpl.cs @@ -0,0 +1,6 @@ +namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.Logging; + +public interface ILogDataStoreImpl +{ + public ILogDataStore DataStore { get; } +} \ No newline at end of file diff --git a/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/LogDataStore.cs b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/LogDataStore.cs new file mode 100644 index 00000000..3f32ea16 --- /dev/null +++ b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/LogDataStore.cs @@ -0,0 +1,38 @@ +using System.Collections.ObjectModel; +using Avalonia.Threading; + +namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.Logging; + +public class LogDataStore : ILogDataStore +{ + public static LogDataStore Instance { get; } = new(); + + #region Fields + + private static readonly SemaphoreSlim _semaphore = new(initialCount: 1); + + #endregion + + #region Properties + + public ObservableCollection Entries { get; } = new(); + + #endregion + + #region Methods + + public virtual void AddEntry(LogModel logModel) + { + // ensure only one operation at time from multiple threads + _semaphore.Wait(); + + Dispatcher.UIThread.Post(() => + { + Entries.Add(logModel); + }); + + _semaphore.Release(); + } + + #endregion +} diff --git a/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/LogEntryColor.cs b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/LogEntryColor.cs new file mode 100644 index 00000000..42c3b6e0 --- /dev/null +++ b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/LogEntryColor.cs @@ -0,0 +1,20 @@ +using System.Drawing; + +namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.Logging; + +public class LogEntryColor +{ + public LogEntryColor() + { + } + + public LogEntryColor(Color foreground, Color background) + { + Foreground = foreground; + Background = background; + } + + public Color Foreground { get; set; } = Color.Black; + public Color Background { get; set; } = Color.Transparent; + +} \ No newline at end of file diff --git a/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/LogModel.cs b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/LogModel.cs new file mode 100644 index 00000000..af1ddca3 --- /dev/null +++ b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/LogModel.cs @@ -0,0 +1,33 @@ +using Microsoft.Extensions.Logging; + +namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.Logging; + +public class LogModel +{ + #region Properties + + public DateTime Timestamp { get; set; } + + public LogLevel LogLevel { get; set; } + + public EventId EventId { get; set; } + + public object? State { get; set; } + + public string? LoggerName { get; set; } + + public string? CallerClassName { get; set; } + + public string? CallerMemberName { get; set; } + + public string? Exception { get; set; } + + public LogEntryColor? Color { get; set; } + + #endregion + + public string LoggerDisplayName => + LoggerName? + .Split('.', StringSplitOptions.RemoveEmptyEntries) + .LastOrDefault() ?? ""; +} diff --git a/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/ViewModels/LogViewerControlViewModel.cs b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/ViewModels/LogViewerControlViewModel.cs new file mode 100644 index 00000000..1789a0e8 --- /dev/null +++ b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/ViewModels/LogViewerControlViewModel.cs @@ -0,0 +1,21 @@ +using StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.Logging; + +namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.ViewModels; + +public class LogViewerControlViewModel : ViewModel, ILogDataStoreImpl +{ + #region Constructor + + public LogViewerControlViewModel(ILogDataStore dataStore) + { + DataStore = dataStore; + } + + #endregion + + #region Properties + + public ILogDataStore DataStore { get; set; } + + #endregion +} \ No newline at end of file diff --git a/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/ViewModels/ObservableObject.cs b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/ViewModels/ObservableObject.cs new file mode 100644 index 00000000..e6792bea --- /dev/null +++ b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/ViewModels/ObservableObject.cs @@ -0,0 +1,21 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.ViewModels; + +public class ObservableObject : INotifyPropertyChanged +{ + protected bool Set(ref TValue field, TValue newValue, [CallerMemberName] string? propertyName = null) + { + if (EqualityComparer.Default.Equals(field, newValue)) return false; + field = newValue; + OnPropertyChanged(propertyName); + + return true; + } + + public event PropertyChangedEventHandler? PropertyChanged; + + protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) + => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); +} diff --git a/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/ViewModels/ViewModel.cs b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/ViewModels/ViewModel.cs new file mode 100644 index 00000000..b7b4aec0 --- /dev/null +++ b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/ViewModels/ViewModel.cs @@ -0,0 +1,3 @@ +namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.ViewModels; + +public class ViewModel : ObservableObject { /* skip */ } diff --git a/StabilityMatrix.Avalonia.Diagnostics/LogViewer/DataStoreLoggerTarget.cs b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/DataStoreLoggerTarget.cs new file mode 100644 index 00000000..608b7245 --- /dev/null +++ b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/DataStoreLoggerTarget.cs @@ -0,0 +1,75 @@ +using System.Diagnostics; +using Microsoft.Extensions.Logging; +using NLog; +using NLog.Targets; +using StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.Logging; +using MsLogLevel = Microsoft.Extensions.Logging.LogLevel; + +namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer; + +[Target("DataStoreLogger")] +public class DataStoreLoggerTarget : TargetWithLayout +{ + #region Fields + + private ILogDataStore? _dataStore; + private DataStoreLoggerConfiguration? _config; + + #endregion + + #region methods + + protected override void InitializeTarget() + { + // we need to inject dependencies + // var serviceProvider = ResolveService(); + + // reference the shared instance + _dataStore = LogDataStore.Instance; + // _dataStore = serviceProvider.GetRequiredService(); + + // load the config options + /*var options + = serviceProvider.GetService>();*/ + + // _config = options?.CurrentValue ?? new DataStoreLoggerConfiguration(); + _config = new DataStoreLoggerConfiguration(); + + base.InitializeTarget(); + } + + protected override void Write(LogEventInfo logEvent) + { + // cast NLog Loglevel to Microsoft LogLevel type + var logLevel = (MsLogLevel)Enum.ToObject(typeof(MsLogLevel), logEvent.Level.Ordinal); + + // format the message + var message = RenderLogEvent(Layout, logEvent); + + // retrieve the EventId + logEvent.Properties.TryGetValue("EventId", out var result); + if (result is not EventId eventId) + { + eventId = _config!.EventId; + } + + // add log entry + _dataStore?.AddEntry(new LogModel + { + Timestamp = DateTime.UtcNow, + LogLevel = logLevel, + // do we override the default EventId if it exists? + EventId = eventId.Id == 0 && (_config?.EventId.Id ?? 0) != 0 ? _config!.EventId : eventId, + State = message, + LoggerName = logEvent.LoggerName, + CallerClassName = logEvent.CallerClassName, + CallerMemberName = logEvent.CallerMemberName, + Exception = logEvent.Exception?.Message ?? (logLevel == MsLogLevel.Error ? message : ""), + Color = _config!.Colors[logLevel], + }); + + Debug.WriteLine($"--- [{logLevel.ToString()[..3]}] {message} - {logEvent.Exception?.Message ?? "no error"}"); + } + + #endregion +} diff --git a/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Extensions/ServicesExtension.cs b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Extensions/ServicesExtension.cs new file mode 100644 index 00000000..9ab61604 --- /dev/null +++ b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Extensions/ServicesExtension.cs @@ -0,0 +1,65 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using NLog; +using StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.Logging; +using StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.ViewModels; +using LogDataStore = StabilityMatrix.Avalonia.Diagnostics.LogViewer.Logging.LogDataStore; +using MsLogLevel = Microsoft.Extensions.Logging.LogLevel; + +namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Extensions; + +[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] +public static class ServicesExtension +{ + public static IServiceCollection AddLogViewer(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + + return services; + } + + public static IServiceCollection AddLogViewer( + this IServiceCollection services, + Action configure) + { + services.AddSingleton(Core.Logging.LogDataStore.Instance); + services.AddSingleton(); + services.Configure(configure); + + return services; + } + + public static ILoggingBuilder AddNLogTargets(this ILoggingBuilder builder, IConfiguration config) + { + LogManager + .Setup() + // Register custom Target + .SetupExtensions(extensionBuilder => + extensionBuilder.RegisterTarget("DataStoreLogger")); + + /*builder + .ClearProviders() + .SetMinimumLevel(MsLogLevel.Trace) + // Load NLog settings from appsettings*.json + .AddNLog(config, + // custom options for capturing the EventId information + new NLogProviderOptions + { + // https://nlog-project.org/2021/08/25/nlog-5-0-preview1-ready.html#nlogextensionslogging-changes-capture-of-eventid + IgnoreEmptyEventId = false, + CaptureEventId = EventIdCaptureType.Legacy + });*/ + + return builder; + } + + public static ILoggingBuilder AddNLogTargets(this ILoggingBuilder builder, IConfiguration config, Action configure) + { + builder.AddNLogTargets(config); + builder.Services.Configure(configure); + return builder; + } +} diff --git a/StabilityMatrix.Avalonia.Diagnostics/LogViewer/LICENSE b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/LICENSE new file mode 100644 index 00000000..2b8b5163 --- /dev/null +++ b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Graeme Grant + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Logging/LogDataStore.cs b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Logging/LogDataStore.cs new file mode 100644 index 00000000..4dbdbf50 --- /dev/null +++ b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Logging/LogDataStore.cs @@ -0,0 +1,13 @@ +using Avalonia.Threading; + +namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Logging; + +public class LogDataStore : Core.Logging.LogDataStore +{ + #region Methods + + public override async void AddEntry(Core.Logging.LogModel logModel) + => await Dispatcher.UIThread.InvokeAsync(() => base.AddEntry(logModel)); + + #endregion +} diff --git a/StabilityMatrix.Avalonia.Diagnostics/LogViewer/README.md b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/README.md new file mode 100644 index 00000000..ff60c426 --- /dev/null +++ b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/README.md @@ -0,0 +1,3 @@ +## LogViewer + +Source code in the `StabilityMatrix.Avalonia.Diagnostics.LogViewer `namespace is included from [CodeProject](https://www.codeproject.com/Articles/5357417/LogViewer-Control-for-WinForms-WPF-and-Avalonia-in) under the [MIT License](LICENSE). \ No newline at end of file diff --git a/StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj b/StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj new file mode 100644 index 00000000..cfbcd729 --- /dev/null +++ b/StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj @@ -0,0 +1,36 @@ + + + + net7.0 + win-x64;linux-x64;osx-x64;osx-arm64 + enable + enable + true + true + + + + + + + + + + + + + + + + + + + + + + + LogWindow.axaml + + + + diff --git a/StabilityMatrix.Avalonia.Diagnostics/ViewModels/LogWindowViewModel.cs b/StabilityMatrix.Avalonia.Diagnostics/ViewModels/LogWindowViewModel.cs new file mode 100644 index 00000000..860e2b68 --- /dev/null +++ b/StabilityMatrix.Avalonia.Diagnostics/ViewModels/LogWindowViewModel.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.DependencyInjection; +using StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.ViewModels; + +namespace StabilityMatrix.Avalonia.Diagnostics.ViewModels; + +public class LogWindowViewModel +{ + public LogViewerControlViewModel LogViewer { get; } + + public LogWindowViewModel(LogViewerControlViewModel logViewer) + { + LogViewer = logViewer; + } + + public static LogWindowViewModel FromServiceProvider(IServiceProvider services) + { + return new LogWindowViewModel( + services.GetRequiredService()); + } +} diff --git a/StabilityMatrix.Avalonia.Diagnostics/Views/LogWindow.axaml b/StabilityMatrix.Avalonia.Diagnostics/Views/LogWindow.axaml new file mode 100644 index 00000000..b0dc3d65 --- /dev/null +++ b/StabilityMatrix.Avalonia.Diagnostics/Views/LogWindow.axaml @@ -0,0 +1,22 @@ + + + + + diff --git a/StabilityMatrix.Avalonia.Diagnostics/Views/LogWindow.axaml.cs b/StabilityMatrix.Avalonia.Diagnostics/Views/LogWindow.axaml.cs new file mode 100644 index 00000000..d3ead407 --- /dev/null +++ b/StabilityMatrix.Avalonia.Diagnostics/Views/LogWindow.axaml.cs @@ -0,0 +1,39 @@ +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using StabilityMatrix.Avalonia.Diagnostics.ViewModels; + +namespace StabilityMatrix.Avalonia.Diagnostics.Views; + +public partial class LogWindow : Window +{ + public LogWindow() + { + InitializeComponent(); + } + + public static IDisposable Attach(TopLevel root, IServiceProvider serviceProvider) + { + return Attach(root, serviceProvider, new KeyGesture(Key.F11)); + } + + public static IDisposable Attach(TopLevel root, IServiceProvider serviceProvider, KeyGesture gesture) + { + return (root ?? throw new ArgumentNullException(nameof(root))).AddDisposableHandler( + KeyDownEvent, + PreviewKeyDown, + RoutingStrategies.Tunnel); + + void PreviewKeyDown(object? sender, KeyEventArgs e) + { + if (gesture.Matches(e)) + { + var window = new LogWindow() + { + DataContext = LogWindowViewModel.FromServiceProvider(serviceProvider) + }; + window.Show(); + } + } + } +} diff --git a/StabilityMatrix.Avalonia/App.axaml.cs b/StabilityMatrix.Avalonia/App.axaml.cs index ca3af39d..bb38fcec 100644 --- a/StabilityMatrix.Avalonia/App.axaml.cs +++ b/StabilityMatrix.Avalonia/App.axaml.cs @@ -33,6 +33,8 @@ using Refit; using Sentry; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.DesignData; +using StabilityMatrix.Avalonia.Diagnostics.LogViewer; +using StabilityMatrix.Avalonia.Diagnostics.LogViewer.Extensions; using StabilityMatrix.Avalonia.Helpers; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Models; @@ -59,6 +61,7 @@ using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Updater; using Application = Avalonia.Application; +using DrawingColor = System.Drawing.Color; using LogLevel = Microsoft.Extensions.Logging.LogLevel; namespace StabilityMatrix.Avalonia; @@ -464,6 +467,8 @@ public sealed class App : Application services.AddHttpClient("A3Client") .AddPolicyHandler(localTimeout.WrapAsync(localRetryPolicy)); + ConditionalAddLogViewer(services); + // Add logging services.AddLogging(builder => { @@ -473,7 +478,16 @@ public sealed class App : Application .AddFilter("Microsoft", LogLevel.Warning) .AddFilter("System", LogLevel.Warning); builder.SetMinimumLevel(LogLevel.Debug); +#if DEBUG + builder.AddNLog(ConfigureLogging(), + new NLogProviderOptions + { + IgnoreEmptyEventId = false, + CaptureEventId = EventIdCaptureType.Legacy + }); +#else builder.AddNLog(ConfigureLogging()); +#endif }); return services; @@ -525,7 +539,11 @@ public sealed class App : Application private static LoggingConfiguration ConfigureLogging() { - LogManager.Setup().LoadConfiguration(builder => { + var setupBuilder = LogManager.Setup(); + + ConditionalAddLogViewerNLog(setupBuilder); + + setupBuilder.LoadConfiguration(builder => { var debugTarget = builder.ForTarget("console").WriteTo(new DebuggerTarget { Layout = "${message}" @@ -548,6 +566,14 @@ public sealed class App : Application builder.ForLogger().FilterMinLevel(NLog.LogLevel.Trace).WriteTo(debugTarget); builder.ForLogger().FilterMinLevel(NLog.LogLevel.Debug).WriteTo(fileTarget); + +#if DEBUG + var logViewerTarget = builder.ForTarget("DataStoreLogger").WriteTo(new DataStoreLoggerTarget() + { + Layout = "${message}" + }); + builder.ForLogger().FilterMinLevel(NLog.LogLevel.Trace).WriteTo(logViewerTarget); +#endif }); // Sentry @@ -567,6 +593,21 @@ public sealed class App : Application }); } + LogManager.ReconfigExistingLoggers(); + return LogManager.Configuration; } + + [Conditional("DEBUG")] + private static void ConditionalAddLogViewer(IServiceCollection services) + { + services.AddLogViewer(); + } + + [Conditional("DEBUG")] + private static void ConditionalAddLogViewerNLog(ISetupBuilder setupBuilder) + { + setupBuilder.SetupExtensions(extensionBuilder => + extensionBuilder.RegisterTarget("DataStoreLogger")); + } } diff --git a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj index 4ba1ba23..542e5d68 100644 --- a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj +++ b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj @@ -15,7 +15,7 @@ - + @@ -39,7 +39,7 @@ - + @@ -59,6 +59,10 @@ + + + + diff --git a/StabilityMatrix.Avalonia/Views/LaunchPageView.axaml.cs b/StabilityMatrix.Avalonia/Views/LaunchPageView.axaml.cs index ea8ee398..fca9f57f 100644 --- a/StabilityMatrix.Avalonia/Views/LaunchPageView.axaml.cs +++ b/StabilityMatrix.Avalonia/Views/LaunchPageView.axaml.cs @@ -1,7 +1,6 @@ using System; using Avalonia.Controls; using Avalonia.Interactivity; -using Avalonia.Markup.Xaml; using Avalonia.Media; using Avalonia.Threading; using AvaloniaEdit; @@ -61,9 +60,4 @@ public partial class LaunchPageView : UserControlBase editor.ScrollToLine(line); }); } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } } diff --git a/StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs b/StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs index 603c432c..64b170e5 100644 --- a/StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs +++ b/StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs @@ -22,6 +22,7 @@ using FluentAvalonia.UI.Windowing; using Microsoft.Extensions.DependencyInjection; using StabilityMatrix.Avalonia.Animations; using StabilityMatrix.Avalonia.Controls; +using StabilityMatrix.Avalonia.Diagnostics.Views; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels; using StabilityMatrix.Avalonia.ViewModels.Base; @@ -52,6 +53,7 @@ public partial class MainWindow : AppWindowBase #if DEBUG this.AttachDevTools(); + LogWindow.Attach(this, App.Services); #endif TitleBar.ExtendsContentIntoTitleBar = true; diff --git a/StabilityMatrix.sln b/StabilityMatrix.sln index 1f97cd45..86751cd2 100644 --- a/StabilityMatrix.sln +++ b/StabilityMatrix.sln @@ -11,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StabilityMatrix.Core", "Sta EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StabilityMatrix.Avalonia", "StabilityMatrix.Avalonia\StabilityMatrix.Avalonia.csproj", "{3F9F96FA-12E7-4B82-BFE5-1FAD88F3ACB2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StabilityMatrix.Avalonia.Diagnostics", "StabilityMatrix.Avalonia.Diagnostics\StabilityMatrix.Avalonia.Diagnostics.csproj", "{6D088B89-12D4-4EA0-BA6B-305C7D10C084}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -31,6 +33,10 @@ Global {3F9F96FA-12E7-4B82-BFE5-1FAD88F3ACB2}.Debug|Any CPU.Build.0 = Debug|Any CPU {3F9F96FA-12E7-4B82-BFE5-1FAD88F3ACB2}.Release|Any CPU.ActiveCfg = Release|Any CPU {3F9F96FA-12E7-4B82-BFE5-1FAD88F3ACB2}.Release|Any CPU.Build.0 = Release|Any CPU + {6D088B89-12D4-4EA0-BA6B-305C7D10C084}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6D088B89-12D4-4EA0-BA6B-305C7D10C084}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6D088B89-12D4-4EA0-BA6B-305C7D10C084}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6D088B89-12D4-4EA0-BA6B-305C7D10C084}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE