From 82f7329a39bb1e348333e74ff75793a274f9dbff Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 25 Aug 2024 21:55:34 +0200 Subject: [PATCH] Refactored logging into a unified logging --- app/MindWork AI Studio/Agents/AgentBase.cs | 6 +- .../Agents/AgentTextContentCleaner.cs | 2 +- .../Assistants/AssistantBase.razor.cs | 7 +- .../Components/Workspaces.razor.cs | 5 +- app/MindWork AI Studio/Pages/Chat.razor.cs | 9 +- .../Pages/Settings.razor.cs | 13 +- app/MindWork AI Studio/Program.cs | 17 + .../Provider/Anthropic/ProviderAnthropic.cs | 2 +- .../Provider/BaseProvider.cs | 10 +- .../Provider/Fireworks/ProviderFireworks.cs | 2 +- .../Provider/Mistral/ProviderMistral.cs | 2 +- .../Provider/OpenAI/ProviderOpenAI.cs | 2 +- app/MindWork AI Studio/Provider/Providers.cs | 61 ---- .../Provider/ProvidersExtensions.cs | 60 ++++ .../Provider/SelfHosted/ProviderSelfHosted.cs | 4 +- .../Settings/ProviderDialog.razor.cs | 13 +- .../Settings/SettingsManager.cs | 32 +- .../Settings/SettingsMigrations.cs | 36 +-- app/MindWork AI Studio/Tools/Rust.cs | 15 +- .../Services/MarkdownClipboardService.cs | 2 +- .../Tools/Services/TemporaryChatService.cs | 18 +- .../Tools/TerminalLogger.cs | 23 ++ runtime/Cargo.toml | 2 +- runtime/src/main.rs | 297 ++++++++++++------ 24 files changed, 416 insertions(+), 224 deletions(-) create mode 100644 app/MindWork AI Studio/Provider/ProvidersExtensions.cs create mode 100644 app/MindWork AI Studio/Tools/TerminalLogger.cs diff --git a/app/MindWork AI Studio/Agents/AgentBase.cs b/app/MindWork AI Studio/Agents/AgentBase.cs index 191ca271..456f0437 100644 --- a/app/MindWork AI Studio/Agents/AgentBase.cs +++ b/app/MindWork AI Studio/Agents/AgentBase.cs @@ -7,13 +7,15 @@ using AIStudio.Tools; namespace AIStudio.Agents; -public abstract class AgentBase(SettingsManager settingsManager, IJSRuntime jsRuntime, ThreadSafeRandom rng) : IAgent +public abstract class AgentBase(ILogger logger, SettingsManager settingsManager, IJSRuntime jsRuntime, ThreadSafeRandom rng) : IAgent { protected SettingsManager SettingsManager { get; init; } = settingsManager; protected IJSRuntime JsRuntime { get; init; } = jsRuntime; protected ThreadSafeRandom RNG { get; init; } = rng; + + protected ILogger Logger { get; init; } = logger; /// /// Represents the type or category of this agent. @@ -104,6 +106,6 @@ public abstract class AgentBase(SettingsManager settingsManager, IJSRuntime jsRu // Use the selected provider to get the AI response. // By awaiting this line, we wait for the entire // content to be streamed. - await aiText.CreateFromProviderAsync(providerSettings.CreateProvider(), this.JsRuntime, this.SettingsManager, providerSettings.Model, thread); + await aiText.CreateFromProviderAsync(providerSettings.CreateProvider(this.Logger), this.JsRuntime, this.SettingsManager, providerSettings.Model, thread); } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Agents/AgentTextContentCleaner.cs b/app/MindWork AI Studio/Agents/AgentTextContentCleaner.cs index 83fe48ff..62172be6 100644 --- a/app/MindWork AI Studio/Agents/AgentTextContentCleaner.cs +++ b/app/MindWork AI Studio/Agents/AgentTextContentCleaner.cs @@ -4,7 +4,7 @@ using AIStudio.Tools; namespace AIStudio.Agents; -public sealed class AgentTextContentCleaner(SettingsManager settingsManager, IJSRuntime jsRuntime, ThreadSafeRandom rng) : AgentBase(settingsManager, jsRuntime, rng) +public sealed class AgentTextContentCleaner(ILogger logger, SettingsManager settingsManager, IJSRuntime jsRuntime, ThreadSafeRandom rng) : AgentBase(logger, settingsManager, jsRuntime, rng) { private static readonly ContentBlock EMPTY_BLOCK = new() { diff --git a/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs b/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs index 496b3b52..bf1e8ddf 100644 --- a/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs +++ b/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs @@ -10,7 +10,7 @@ namespace AIStudio.Assistants; public abstract partial class AssistantBase : ComponentBase { [Inject] - protected SettingsManager SettingsManager { get; set; } = null!; + protected SettingsManager SettingsManager { get; init; } = null!; [Inject] protected IJSRuntime JsRuntime { get; init; } = null!; @@ -27,6 +27,9 @@ public abstract partial class AssistantBase : ComponentBase [Inject] protected NavigationManager NavigationManager { get; init; } = null!; + [Inject] + protected ILogger Logger { get; init; } = null!; + internal const string AFTER_RESULT_DIV_ID = "afterAssistantResult"; internal const string RESULT_DIV_ID = "assistantResult"; @@ -151,7 +154,7 @@ public abstract partial class AssistantBase : ComponentBase // Use the selected provider to get the AI response. // By awaiting this line, we wait for the entire // content to be streamed. - await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(), this.JsRuntime, this.SettingsManager, this.providerSettings.Model, this.chatThread); + await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.JsRuntime, this.SettingsManager, this.providerSettings.Model, this.chatThread); this.isProcessing = false; this.StateHasChanged(); diff --git a/app/MindWork AI Studio/Components/Workspaces.razor.cs b/app/MindWork AI Studio/Components/Workspaces.razor.cs index ee33e64c..c57d1aa1 100644 --- a/app/MindWork AI Studio/Components/Workspaces.razor.cs +++ b/app/MindWork AI Studio/Components/Workspaces.razor.cs @@ -24,6 +24,9 @@ public partial class Workspaces : ComponentBase [Inject] private ThreadSafeRandom RNG { get; init; } = null!; + [Inject] + private ILogger Logger { get; init; } = null!; + [Parameter] public ChatThread? CurrentChatThread { get; set; } @@ -309,7 +312,7 @@ public partial class Workspaces : ComponentBase } catch (Exception e) { - Console.WriteLine(e); + this.Logger.LogError($"Failed to load chat from '{chatPath}': {e.Message}"); } return null; diff --git a/app/MindWork AI Studio/Pages/Chat.razor.cs b/app/MindWork AI Studio/Pages/Chat.razor.cs index b9b1c3f7..fef3287b 100644 --- a/app/MindWork AI Studio/Pages/Chat.razor.cs +++ b/app/MindWork AI Studio/Pages/Chat.razor.cs @@ -19,7 +19,7 @@ namespace AIStudio.Pages; public partial class Chat : MSGComponentBase, IAsyncDisposable { [Inject] - private SettingsManager SettingsManager { get; set; } = null!; + private SettingsManager SettingsManager { get; init; } = null!; [Inject] public IJSRuntime JsRuntime { get; init; } = null!; @@ -28,7 +28,10 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable private ThreadSafeRandom RNG { get; init; } = null!; [Inject] - public IDialogService DialogService { get; set; } = null!; + private IDialogService DialogService { get; init; } = null!; + + [Inject] + private ILogger Logger { get; init; } = null!; private InnerScrolling scrollingArea = null!; @@ -189,7 +192,7 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable // Use the selected provider to get the AI response. // By awaiting this line, we wait for the entire // content to be streamed. - await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(), this.JsRuntime, this.SettingsManager, this.providerSettings.Model, this.chatThread); + await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.JsRuntime, this.SettingsManager, this.providerSettings.Model, this.chatThread); // Save the chat: if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY) diff --git a/app/MindWork AI Studio/Pages/Settings.razor.cs b/app/MindWork AI Studio/Pages/Settings.razor.cs index fee38abb..debf62a9 100644 --- a/app/MindWork AI Studio/Pages/Settings.razor.cs +++ b/app/MindWork AI Studio/Pages/Settings.razor.cs @@ -14,16 +14,19 @@ namespace AIStudio.Pages; public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable { [Inject] - public SettingsManager SettingsManager { get; init; } = null!; + private SettingsManager SettingsManager { get; init; } = null!; [Inject] - public IDialogService DialogService { get; init; } = null!; + private IDialogService DialogService { get; init; } = null!; [Inject] - public IJSRuntime JsRuntime { get; init; } = null!; + private IJSRuntime JsRuntime { get; init; } = null!; [Inject] - protected MessageBus MessageBus { get; init; } = null!; + private MessageBus MessageBus { get; init; } = null!; + + [Inject] + private ILogger Logger { get; init; } = null!; private readonly List> availableProviders = new(); @@ -111,7 +114,7 @@ public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable if (dialogResult is null || dialogResult.Canceled) return; - var providerInstance = provider.CreateProvider(); + var providerInstance = provider.CreateProvider(this.Logger); var deleteSecretResponse = await this.SettingsManager.DeleteAPIKey(this.JsRuntime, providerInstance); if(deleteSecretResponse.Success) { diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs index 30a6ef16..7e6ee1e1 100644 --- a/app/MindWork AI Studio/Program.cs +++ b/app/MindWork AI Studio/Program.cs @@ -4,6 +4,8 @@ using AIStudio.Settings; using AIStudio.Tools; using AIStudio.Tools.Services; +using Microsoft.Extensions.Logging.Console; + using MudBlazor.Services; #if !DEBUG @@ -35,6 +37,18 @@ if(string.IsNullOrWhiteSpace(secretKey)) } var builder = WebApplication.CreateBuilder(); +builder.Logging.ClearProviders(); +builder.Logging.SetMinimumLevel(LogLevel.Debug); +builder.Logging.AddFilter("Microsoft", LogLevel.Information); +builder.Logging.AddFilter("Microsoft.AspNetCore.Hosting.Diagnostics", LogLevel.Warning); +builder.Logging.AddFilter("Microsoft.AspNetCore.Routing.EndpointMiddleware", LogLevel.Warning); +builder.Logging.AddFilter("Microsoft.AspNetCore.StaticFiles", LogLevel.Warning); +builder.Logging.AddFilter("MudBlazor", LogLevel.Information); +builder.Logging.AddConsole(options => +{ + options.FormatterName = TerminalLogger.FORMATTER_NAME; +}).AddConsoleFormatter(); + builder.Services.AddMudServices(config => { config.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.BottomLeft; @@ -101,5 +115,8 @@ app.MapRazorComponents() var serverTask = app.RunAsync(); +var rustLogger = app.Services.GetRequiredService>(); +rust.SetLogger(rustLogger); + await rust.AppIsReady(); await serverTask; \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs b/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs index ffeb2559..642bbffb 100644 --- a/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs +++ b/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs @@ -8,7 +8,7 @@ using AIStudio.Settings; namespace AIStudio.Provider.Anthropic; -public sealed class ProviderAnthropic() : BaseProvider("https://api.anthropic.com/v1/"), IProvider +public sealed class ProviderAnthropic(ILogger logger) : BaseProvider("https://api.anthropic.com/v1/", logger), IProvider { private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new() { diff --git a/app/MindWork AI Studio/Provider/BaseProvider.cs b/app/MindWork AI Studio/Provider/BaseProvider.cs index d6ce8a54..913d0879 100644 --- a/app/MindWork AI Studio/Provider/BaseProvider.cs +++ b/app/MindWork AI Studio/Provider/BaseProvider.cs @@ -9,13 +9,21 @@ public abstract class BaseProvider /// The HTTP client to use for all requests. /// protected readonly HttpClient httpClient = new(); + + /// + /// The logger to use. + /// + protected readonly ILogger logger; /// /// Constructor for the base provider. /// /// The base URL for the provider. - protected BaseProvider(string url) + /// The logger service to use. + protected BaseProvider(string url, ILogger loggerService) { + this.logger = loggerService; + // Set the base URL: this.httpClient.BaseAddress = new(url); } diff --git a/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs b/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs index cf29a6df..190db509 100644 --- a/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs +++ b/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs @@ -8,7 +8,7 @@ using AIStudio.Settings; namespace AIStudio.Provider.Fireworks; -public class ProviderFireworks() : BaseProvider("https://api.fireworks.ai/inference/v1/"), IProvider +public class ProviderFireworks(ILogger logger) : BaseProvider("https://api.fireworks.ai/inference/v1/", logger), IProvider { private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new() { diff --git a/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs b/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs index c3510811..160e0008 100644 --- a/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs +++ b/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs @@ -9,7 +9,7 @@ using AIStudio.Settings; namespace AIStudio.Provider.Mistral; -public sealed class ProviderMistral() : BaseProvider("https://api.mistral.ai/v1/"), IProvider +public sealed class ProviderMistral(ILogger logger) : BaseProvider("https://api.mistral.ai/v1/", logger), IProvider { private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new() { diff --git a/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs b/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs index 6e4af600..ca0c1769 100644 --- a/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs +++ b/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs @@ -11,7 +11,7 @@ namespace AIStudio.Provider.OpenAI; /// /// The OpenAI provider. /// -public sealed class ProviderOpenAI() : BaseProvider("https://api.openai.com/v1/"), IProvider +public sealed class ProviderOpenAI(ILogger logger) : BaseProvider("https://api.openai.com/v1/", logger), IProvider { private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new() { diff --git a/app/MindWork AI Studio/Provider/Providers.cs b/app/MindWork AI Studio/Provider/Providers.cs index 530d0237..2d5cd295 100644 --- a/app/MindWork AI Studio/Provider/Providers.cs +++ b/app/MindWork AI Studio/Provider/Providers.cs @@ -1,9 +1,3 @@ -using AIStudio.Provider.Anthropic; -using AIStudio.Provider.Fireworks; -using AIStudio.Provider.Mistral; -using AIStudio.Provider.OpenAI; -using AIStudio.Provider.SelfHosted; - namespace AIStudio.Provider; /// @@ -20,59 +14,4 @@ public enum Providers FIREWORKS = 5, SELF_HOSTED = 4, -} - -/// -/// Extension methods for the provider enum. -/// -public static class ExtensionsProvider -{ - /// - /// Returns the human-readable name of the provider. - /// - /// The provider. - /// The human-readable name of the provider. - public static string ToName(this Providers provider) => provider switch - { - Providers.NONE => "No provider selected", - - Providers.OPEN_AI => "OpenAI", - Providers.ANTHROPIC => "Anthropic", - Providers.MISTRAL => "Mistral", - - Providers.FIREWORKS => "Fireworks.ai", - - Providers.SELF_HOSTED => "Self-hosted", - - _ => "Unknown", - }; - - /// - /// Creates a new provider instance based on the provider value. - /// - /// The provider settings. - /// The provider instance. - public static IProvider CreateProvider(this Settings.Provider providerSettings) - { - try - { - return providerSettings.UsedProvider switch - { - Providers.OPEN_AI => new ProviderOpenAI { InstanceName = providerSettings.InstanceName }, - Providers.ANTHROPIC => new ProviderAnthropic { InstanceName = providerSettings.InstanceName }, - Providers.MISTRAL => new ProviderMistral { InstanceName = providerSettings.InstanceName }, - - Providers.FIREWORKS => new ProviderFireworks { InstanceName = providerSettings.InstanceName }, - - Providers.SELF_HOSTED => new ProviderSelfHosted(providerSettings) { InstanceName = providerSettings.InstanceName }, - - _ => new NoProvider(), - }; - } - catch (Exception e) - { - Console.WriteLine($"Failed to create provider: {e.Message}"); - return new NoProvider(); - } - } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/ProvidersExtensions.cs b/app/MindWork AI Studio/Provider/ProvidersExtensions.cs new file mode 100644 index 00000000..806c3bb8 --- /dev/null +++ b/app/MindWork AI Studio/Provider/ProvidersExtensions.cs @@ -0,0 +1,60 @@ +using AIStudio.Provider.Anthropic; +using AIStudio.Provider.Fireworks; +using AIStudio.Provider.Mistral; +using AIStudio.Provider.OpenAI; +using AIStudio.Provider.SelfHosted; + +namespace AIStudio.Provider; + +public static class ProvidersExtensions +{ + /// + /// Returns the human-readable name of the provider. + /// + /// The provider. + /// The human-readable name of the provider. + public static string ToName(this Providers provider) => provider switch + { + Providers.NONE => "No provider selected", + + Providers.OPEN_AI => "OpenAI", + Providers.ANTHROPIC => "Anthropic", + Providers.MISTRAL => "Mistral", + + Providers.FIREWORKS => "Fireworks.ai", + + Providers.SELF_HOSTED => "Self-hosted", + + _ => "Unknown", + }; + + /// + /// Creates a new provider instance based on the provider value. + /// + /// The provider settings. + /// The logger to use. + /// The provider instance. + public static IProvider CreateProvider(this Settings.Provider providerSettings, ILogger logger) + { + try + { + return providerSettings.UsedProvider switch + { + Providers.OPEN_AI => new ProviderOpenAI(logger) { InstanceName = providerSettings.InstanceName }, + Providers.ANTHROPIC => new ProviderAnthropic(logger) { InstanceName = providerSettings.InstanceName }, + Providers.MISTRAL => new ProviderMistral(logger) { InstanceName = providerSettings.InstanceName }, + + Providers.FIREWORKS => new ProviderFireworks(logger) { InstanceName = providerSettings.InstanceName }, + + Providers.SELF_HOSTED => new ProviderSelfHosted(logger, providerSettings) { InstanceName = providerSettings.InstanceName }, + + _ => new NoProvider(), + }; + } + catch (Exception e) + { + logger.LogError($"Failed to create provider: {e.Message}"); + return new NoProvider(); + } + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs b/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs index 623c3b17..e80611cc 100644 --- a/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs +++ b/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs @@ -8,7 +8,7 @@ using AIStudio.Settings; namespace AIStudio.Provider.SelfHosted; -public sealed class ProviderSelfHosted(Settings.Provider provider) : BaseProvider($"{provider.Hostname}{provider.Host.BaseURL()}"), IProvider +public sealed class ProviderSelfHosted(ILogger logger, Settings.Provider provider) : BaseProvider($"{provider.Hostname}{provider.Host.BaseURL()}", logger), IProvider { private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new() { @@ -162,7 +162,7 @@ public sealed class ProviderSelfHosted(Settings.Provider provider) : BaseProvide } catch(Exception e) { - Console.WriteLine($"Failed to load text models from self-hosted provider: {e.Message}"); + this.logger.LogError($"Failed to load text models from self-hosted provider: {e.Message}"); return []; } } diff --git a/app/MindWork AI Studio/Settings/ProviderDialog.razor.cs b/app/MindWork AI Studio/Settings/ProviderDialog.razor.cs index 4d688a0b..1adb775e 100644 --- a/app/MindWork AI Studio/Settings/ProviderDialog.razor.cs +++ b/app/MindWork AI Studio/Settings/ProviderDialog.razor.cs @@ -71,10 +71,13 @@ public partial class ProviderDialog : ComponentBase public bool IsEditing { get; init; } [Inject] - private SettingsManager SettingsManager { get; set; } = null!; + private SettingsManager SettingsManager { get; init; } = null!; [Inject] - private IJSRuntime JsRuntime { get; set; } = null!; + private IJSRuntime JsRuntime { get; init; } = null!; + + [Inject] + private ILogger Logger { get; init; } = null!; private static readonly Dictionary SPELLCHECK_ATTRIBUTES = new(); @@ -133,7 +136,7 @@ public partial class ProviderDialog : ComponentBase } var loadedProviderSettings = this.CreateProviderSettings(); - var provider = loadedProviderSettings.CreateProvider(); + var provider = loadedProviderSettings.CreateProvider(this.Logger); if(provider is NoProvider) { await base.OnInitializedAsync(); @@ -187,7 +190,7 @@ public partial class ProviderDialog : ComponentBase if (addedProviderSettings.UsedProvider != Providers.SELF_HOSTED) { // We need to instantiate the provider to store the API key: - var provider = addedProviderSettings.CreateProvider(); + var provider = addedProviderSettings.CreateProvider(this.Logger); // Store the API key in the OS secure storage: var storeResponse = await this.SettingsManager.SetAPIKey(this.JsRuntime, provider, this.dataAPIKey); @@ -318,7 +321,7 @@ public partial class ProviderDialog : ComponentBase private async Task ReloadModels() { var currentProviderSettings = this.CreateProviderSettings(); - var provider = currentProviderSettings.CreateProvider(); + var provider = currentProviderSettings.CreateProvider(this.Logger); if(provider is NoProvider) return; diff --git a/app/MindWork AI Studio/Settings/SettingsManager.cs b/app/MindWork AI Studio/Settings/SettingsManager.cs index c4cbc443..14131474 100644 --- a/app/MindWork AI Studio/Settings/SettingsManager.cs +++ b/app/MindWork AI Studio/Settings/SettingsManager.cs @@ -11,7 +11,7 @@ namespace AIStudio.Settings; /// /// The settings manager. /// -public sealed class SettingsManager +public sealed class SettingsManager(ILogger logger) { private const string SETTINGS_FILENAME = "settings.json"; @@ -20,6 +20,8 @@ public sealed class SettingsManager WriteIndented = true, Converters = { new JsonStringEnumConverter() }, }; + + private ILogger logger = logger; /// /// The directory where the configuration files are stored. @@ -102,12 +104,18 @@ public sealed class SettingsManager public async Task LoadSettings() { if(!this.IsSetUp) + { + this.logger.LogWarning("Cannot load settings, because the configuration is not set up yet."); return; - + } + var settingsPath = Path.Combine(ConfigDirectory!, SETTINGS_FILENAME); if(!File.Exists(settingsPath)) + { + this.logger.LogWarning("Cannot load settings, because the settings file does not exist."); return; - + } + // We read the `"Version": "V3"` line to determine the version of the settings file: await foreach (var line in File.ReadLinesAsync(settingsPath)) { @@ -123,16 +131,16 @@ public sealed class SettingsManager Enum.TryParse(settingsVersionText, out Version settingsVersion); if(settingsVersion is Version.UNKNOWN) { - Console.WriteLine("Error: Unknown version of the settings file."); + this.logger.LogError("Unknown version of the settings file found."); this.ConfigurationData = new(); return; } - this.ConfigurationData = SettingsMigrations.Migrate(settingsVersion, await File.ReadAllTextAsync(settingsPath), JSON_OPTIONS); + this.ConfigurationData = SettingsMigrations.Migrate(this.logger, settingsVersion, await File.ReadAllTextAsync(settingsPath), JSON_OPTIONS); return; } - Console.WriteLine("Error: Failed to read the version of the settings file."); + this.logger.LogError("Failed to read the version of the settings file."); this.ConfigurationData = new(); } @@ -142,14 +150,22 @@ public sealed class SettingsManager public async Task StoreSettings() { if(!this.IsSetUp) + { + this.logger.LogWarning("Cannot store settings, because the configuration is not set up yet."); return; - + } + var settingsPath = Path.Combine(ConfigDirectory!, SETTINGS_FILENAME); if(!Directory.Exists(ConfigDirectory)) + { + this.logger.LogInformation("Creating the configuration directory."); Directory.CreateDirectory(ConfigDirectory!); - + } + var settingsJson = JsonSerializer.Serialize(this.ConfigurationData, JSON_OPTIONS); await File.WriteAllTextAsync(settingsPath, settingsJson); + + this.logger.LogInformation("Stored the settings to the file system."); } public void InjectSpellchecking(Dictionary attributes) => attributes["spellcheck"] = this.ConfigurationData.App.EnableSpellchecking ? "true" : "false"; diff --git a/app/MindWork AI Studio/Settings/SettingsMigrations.cs b/app/MindWork AI Studio/Settings/SettingsMigrations.cs index 121d96c9..9c5bd3a8 100644 --- a/app/MindWork AI Studio/Settings/SettingsMigrations.cs +++ b/app/MindWork AI Studio/Settings/SettingsMigrations.cs @@ -9,7 +9,7 @@ namespace AIStudio.Settings; public static class SettingsMigrations { - public static Data Migrate(Version previousVersion, string configData, JsonSerializerOptions jsonOptions) + public static Data Migrate(ILogger logger, Version previousVersion, string configData, JsonSerializerOptions jsonOptions) { switch (previousVersion) { @@ -17,41 +17,41 @@ public static class SettingsMigrations var configV1 = JsonSerializer.Deserialize(configData, jsonOptions); if (configV1 is null) { - Console.WriteLine("Error: failed to parse the configuration. Using default values."); + logger.LogError("Failed to parse the v1 configuration. Using default values."); return new(); } - configV1 = MigrateV1ToV2(configV1); - configV1 = MigrateV2ToV3(configV1); - return MigrateV3ToV4(configV1); + configV1 = MigrateV1ToV2(logger, configV1); + configV1 = MigrateV2ToV3(logger, configV1); + return MigrateV3ToV4(logger, configV1); case Version.V2: var configV2 = JsonSerializer.Deserialize(configData, jsonOptions); if (configV2 is null) { - Console.WriteLine("Error: failed to parse the configuration. Using default values."); + logger.LogError("Failed to parse the v2 configuration. Using default values."); return new(); } - configV2 = MigrateV2ToV3(configV2); - return MigrateV3ToV4(configV2); + configV2 = MigrateV2ToV3(logger, configV2); + return MigrateV3ToV4(logger, configV2); case Version.V3: var configV3 = JsonSerializer.Deserialize(configData, jsonOptions); if (configV3 is null) { - Console.WriteLine("Error: failed to parse the configuration. Using default values."); + logger.LogError("Failed to parse the v3 configuration. Using default values."); return new(); } - return MigrateV3ToV4(configV3); + return MigrateV3ToV4(logger, configV3); default: - Console.WriteLine("No configuration migration needed."); + logger.LogInformation("No configuration migration is needed."); var configV4 = JsonSerializer.Deserialize(configData, jsonOptions); if (configV4 is null) { - Console.WriteLine("Error: failed to parse the configuration. Using default values."); + logger.LogError("Failed to parse the v4 configuration. Using default values."); return new(); } @@ -59,14 +59,14 @@ public static class SettingsMigrations } } - private static DataV1V3 MigrateV1ToV2(DataV1V3 previousData) + private static DataV1V3 MigrateV1ToV2(ILogger logger, DataV1V3 previousData) { // // Summary: // In v1 we had no self-hosted providers. Thus, we had no hostnames. // - Console.WriteLine("Migrating from v1 to v2..."); + logger.LogInformation("Migrating from v1 to v2..."); return new() { Version = Version.V2, @@ -81,14 +81,14 @@ public static class SettingsMigrations }; } - private static DataV1V3 MigrateV2ToV3(DataV1V3 previousData) + private static DataV1V3 MigrateV2ToV3(ILogger logger, DataV1V3 previousData) { // // Summary: // In v2, self-hosted providers had no host (LM Studio, llama.cpp, ollama, etc.) // - Console.WriteLine("Migrating from v2 to v3..."); + logger.LogInformation("Migrating from v2 to v3..."); return new() { Version = Version.V3, @@ -110,14 +110,14 @@ public static class SettingsMigrations }; } - private static Data MigrateV3ToV4(DataV1V3 previousConfig) + private static Data MigrateV3ToV4(ILogger logger, DataV1V3 previousConfig) { // // Summary: // We grouped the settings into different categories. // - Console.WriteLine("Migrating from v3 to v4..."); + logger.LogInformation("Migrating from v3 to v4..."); return new() { Version = Version.V4, diff --git a/app/MindWork AI Studio/Tools/Rust.cs b/app/MindWork AI Studio/Tools/Rust.cs index 5bcdb5d4..fcfedc75 100644 --- a/app/MindWork AI Studio/Tools/Rust.cs +++ b/app/MindWork AI Studio/Tools/Rust.cs @@ -9,6 +9,13 @@ public sealed class Rust(string apiPort) : IDisposable { BaseAddress = new Uri($"http://127.0.0.1:{apiPort}"), }; + + private ILogger? logger; + + public void SetLogger(ILogger logService) + { + this.logger = logService; + } public async Task GetAppPort() { @@ -36,14 +43,14 @@ public sealed class Rust(string apiPort) : IDisposable var response = await initialHttp.GetAsync(url); if (!response.IsSuccessStatusCode) { - Console.WriteLine($" Try {tris}/{MAX_TRIES}"); + Console.WriteLine($"Try {tris}/{MAX_TRIES} to get the app port from Rust runtime"); await Task.Delay(wait4Try); continue; } var appPortContent = await response.Content.ReadAsStringAsync(); var appPort = int.Parse(appPortContent); - Console.WriteLine($" Received app port from Rust runtime: '{appPort}'"); + Console.WriteLine($"Received app port from Rust runtime: '{appPort}'"); return appPort; } @@ -54,11 +61,11 @@ public sealed class Rust(string apiPort) : IDisposable public async Task AppIsReady() { const string URL = "/system/dotnet/ready"; - Console.WriteLine($"Notifying Rust runtime that the app is ready."); + this.logger!.LogInformation("Notifying Rust runtime that the app is ready."); var response = await this.http.PostAsync(URL, new StringContent(string.Empty)); if (!response.IsSuccessStatusCode) { - Console.WriteLine($"Failed to notify Rust runtime that the app is ready: '{response.StatusCode}'"); + this.logger!.LogError($"Failed to notify Rust runtime that the app is ready: '{response.StatusCode}'"); } } diff --git a/app/MindWork AI Studio/Tools/Services/MarkdownClipboardService.cs b/app/MindWork AI Studio/Tools/Services/MarkdownClipboardService.cs index 126c13f7..2684b2a9 100644 --- a/app/MindWork AI Studio/Tools/Services/MarkdownClipboardService.cs +++ b/app/MindWork AI Studio/Tools/Services/MarkdownClipboardService.cs @@ -15,7 +15,7 @@ public sealed class MarkdownClipboardService(Rust rust, IJSRuntime jsRuntime, IS private Rust Rust { get; } = rust; /// - /// Gets called when the user wants to copy the markdown to the clipboard. + /// Gets called when the user wants to copy the Markdown to the clipboard. /// /// The Markdown text to copy. public async ValueTask CopyToClipboardAsync(string text) => await this.Rust.CopyText2Clipboard(this.JsRuntime, this.Snackbar, text); diff --git a/app/MindWork AI Studio/Tools/Services/TemporaryChatService.cs b/app/MindWork AI Studio/Tools/Services/TemporaryChatService.cs index 0290298f..f1e49d7f 100644 --- a/app/MindWork AI Studio/Tools/Services/TemporaryChatService.cs +++ b/app/MindWork AI Studio/Tools/Services/TemporaryChatService.cs @@ -3,11 +3,13 @@ using AIStudio.Settings.DataModel; namespace AIStudio.Tools.Services; -public class TemporaryChatService(SettingsManager settingsManager) : BackgroundService +public class TemporaryChatService(ILogger logger, SettingsManager settingsManager) : BackgroundService { private static readonly TimeSpan CHECK_INTERVAL = TimeSpan.FromDays(1); private static bool IS_INITIALIZED; + private readonly ILogger logger = logger; + #region Overrides of BackgroundService protected override async Task ExecuteAsync(CancellationToken stoppingToken) @@ -15,10 +17,12 @@ public class TemporaryChatService(SettingsManager settingsManager) : BackgroundS while (!stoppingToken.IsCancellationRequested && !IS_INITIALIZED) await Task.Delay(TimeSpan.FromSeconds(3), stoppingToken); + this.logger.LogInformation("The temporary chat maintenance service was initialized."); + await settingsManager.LoadSettings(); if(settingsManager.ConfigurationData.Workspace.StorageTemporaryMaintenancePolicy is WorkspaceStorageTemporaryMaintenancePolicy.NO_AUTOMATIC_MAINTENANCE) { - Console.WriteLine("Automatic maintenance of temporary chat storage is disabled. Exiting maintenance service."); + this.logger.LogWarning("Automatic maintenance of temporary chat storage is disabled. Exiting maintenance service."); return; } @@ -34,10 +38,14 @@ public class TemporaryChatService(SettingsManager settingsManager) : BackgroundS private Task StartMaintenance() { + this.logger.LogInformation("Starting maintenance of temporary chat storage."); var temporaryDirectories = Path.Join(SettingsManager.DataDirectory, "tempChats"); if(!Directory.Exists(temporaryDirectories)) + { + this.logger.LogWarning("Temporary chat storage directory does not exist. End maintenance."); return Task.CompletedTask; - + } + foreach (var tempChatDirPath in Directory.EnumerateDirectories(temporaryDirectories)) { var chatPath = Path.Join(tempChatDirPath, "thread.json"); @@ -59,9 +67,13 @@ public class TemporaryChatService(SettingsManager settingsManager) : BackgroundS }; if(deleteChat) + { + this.logger.LogInformation($"Deleting temporary chat storage directory '{tempChatDirPath}' due to maintenance policy."); Directory.Delete(tempChatDirPath, true); + } } + this.logger.LogInformation("Finished maintenance of temporary chat storage."); return Task.CompletedTask; } diff --git a/app/MindWork AI Studio/Tools/TerminalLogger.cs b/app/MindWork AI Studio/Tools/TerminalLogger.cs new file mode 100644 index 00000000..ce87feb4 --- /dev/null +++ b/app/MindWork AI Studio/Tools/TerminalLogger.cs @@ -0,0 +1,23 @@ +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Logging.Console; + +namespace AIStudio.Tools; + +public sealed class TerminalLogger() : ConsoleFormatter(FORMATTER_NAME) +{ + public const string FORMATTER_NAME = "AI Studio Terminal Logger"; + + public override void Write(in LogEntry logEntry, IExternalScopeProvider? scopeProvider, TextWriter textWriter) + { + var message = logEntry.Formatter(logEntry.State, logEntry.Exception); + var timestamp = DateTimeOffset.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fff"); + var logLevel = logEntry.LogLevel.ToString(); + var category = logEntry.Category; + + textWriter.Write($"=> {timestamp} [{logLevel}] {category}: {message}"); + if (logEntry.Exception is not null) + textWriter.Write($" Exception was = {logEntry.Exception}"); + + textWriter.WriteLine(); + } +} \ No newline at end of file diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index e8c3333c..8c7a70e5 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -17,7 +17,7 @@ keyring = { version = "3.2", features = ["apple-native", "windows-native", "sync arboard = "3.4.0" tokio = { version = "1.39", features = ["rt", "rt-multi-thread", "macros"] } flexi_logger = "0.28" -log = "0.4" +log = { version = "0.4", features = ["kv"] } once_cell = "1.19.0" rocket = { version = "0.5", default-features = false, features = ["json"] } rand = "0.8" diff --git a/runtime/src/main.rs b/runtime/src/main.rs index c514bc81..7b2fd97d 100644 --- a/runtime/src/main.rs +++ b/runtime/src/main.rs @@ -4,9 +4,11 @@ extern crate rocket; extern crate core; -use std::collections::HashSet; +use std::collections::{BTreeMap, HashMap, HashSet}; +use std::iter::once; use std::net::TcpListener; use std::sync::{Arc, Mutex}; +use std::fmt::Write; use once_cell::sync::Lazy; use arboard::Clipboard; @@ -15,9 +17,12 @@ use serde::Serialize; use tauri::{Manager, Url, Window}; use tauri::api::process::{Command, CommandChild, CommandEvent}; use tokio::time; -use flexi_logger::{AdaptiveFormat, Logger}; +use flexi_logger::{DeferredNow, Duplicate, FileSpec, Logger}; +use flexi_logger::writers::FileLogWriter; use keyring::error::Error::NoEntry; -use log::{debug, error, info, warn}; +use log::{debug, error, info, kv, warn}; +use log::kv::{Key, Value, VisitSource}; +use rand::{RngCore, SeedableRng}; use rocket::figment::Figment; use rocket::{get, post, routes}; use rocket::config::Shutdown; @@ -65,16 +70,36 @@ async fn main() { let tauri_version = metadata_lines.next().unwrap(); let app_commit_hash = metadata_lines.next().unwrap(); - // Set the log level according to the environment: - // In debug mode, the log level is set to debug, in release mode to info. - let log_level = match is_dev() { - true => "debug", - false => "info", + // + // Configure the logger: + // + let mut log_config = String::new(); + + // Set the log level depending on the environment: + match is_dev() { + true => log_config.push_str("debug, "), + false => log_config.push_str("info, "), }; - Logger::try_with_str(log_level).expect("Cannot create logging") - .log_to_stdout() - .adaptive_format_for_stdout(AdaptiveFormat::Detailed) + // Set the log level for the Rocket library: + log_config.push_str("rocket=info, "); + + // Set the log level for the Rocket server: + log_config.push_str("rocket::server=warn, "); + + // Set the log level for the Reqwest library: + log_config.push_str("reqwest::async_impl::client=info"); + + let logger = Logger::try_with_str(log_config).expect("Cannot create logging") + .log_to_file(FileSpec::default() + .basename("AI Studio Events") + .suppress_timestamp() + .suffix("log")) + .duplicate_to_stdout(Duplicate::All) + .use_utc() + .format_for_files(file_logger_format) + .format_for_stderr(terminal_colored_logger_format) + .format_for_stdout(terminal_colored_logger_format) .start().expect("Cannot start logging"); info!("Starting MindWork AI Studio:"); @@ -94,6 +119,8 @@ async fn main() { let api_port = *API_SERVER_PORT; info!("Try to start the API server on 'http://localhost:{api_port}'..."); + + // Configure the runtime API server: let figment = Figment::from(rocket::Config::release_default()) // We use the next available port which was determined before: @@ -112,6 +139,9 @@ async fn main() { .merge(("workers", 3)) .merge(("max_blocking", 12)) + // No colors and emojis in the log output: + .merge(("cli_colors", false)) + // Set the shutdown configuration: .merge(("shutdown", Shutdown { @@ -125,6 +155,7 @@ async fn main() { ..Shutdown::default() })); + // // Start the runtime API server in a separate thread. This is necessary // because the server is blocking, and we need to run the Tauri app in // parallel: @@ -134,6 +165,7 @@ async fn main() { .mount("/", routes![dotnet_port, dotnet_ready]) .ignite().await.unwrap() .launch().await.unwrap(); + }); // // Generate a secret key for the AES encryption for the IPC channel: @@ -208,56 +240,33 @@ async fn main() { // Log the output of the .NET server: while let Some(CommandEvent::Stdout(line)) = rx.recv().await { - _ if line_cleared.contains("fail") || line_cleared.contains("error") || line_cleared.contains("exception") => _ = sender.send(ServerEvent::Error(line)).await, - _ if line_cleared.contains("warn") => _ = sender.send(ServerEvent::Warning(line)).await, - _ if line_cleared.contains("404") => _ = sender.send(ServerEvent::NotFound(line)).await, - _ => (), + // Remove newline characters from the end: + let line = line.trim_end(); + + // Starts the line with '=>'? + if line.starts_with("=>") { + // Yes. This means that the line is a log message from the .NET server. + // The format is: ' [] : '. + // We try to parse this line and log it with the correct log level: + let line = line.trim_start_matches("=>").trim(); + let parts = line.split_once(": ").unwrap(); + let left_part = parts.0.trim(); + let message = parts.1.trim(); + let parts = left_part.split_once("] ").unwrap(); + let level = parts.0.split_once("[").unwrap().1.trim(); + let source = parts.1.trim(); + match level { + "Trace" => debug!(Source = ".NET Server", Comp = source; "{message}"), + "Debug" => debug!(Source = ".NET Server", Comp = source; "{message}"), + "Information" => info!(Source = ".NET Server", Comp = source; "{message}"), + "Warning" => warn!(Source = ".NET Server", Comp = source; "{message}"), + "Error" => error!(Source = ".NET Server", Comp = source; "{message}"), + "Critical" => error!(Source = ".NET Server", Comp = source; "{message}"), + + _ => error!(Source = ".NET Server", Comp = source; "{message} (unknown log level '{level}')"), } - } - - let sending_stop_result = sender.send(ServerEvent::Stopped).await; - match sending_stop_result { - Ok(_) => (), - Err(e) => error!("Was not able to send the server stop message: {e}."), - } - }); - } else { - warn!("Running in development mode, no .NET server will be started."); - } - - // TODO: Migrate logging to runtime API server: - let server_receive_clone = DOTNET_SERVER.clone(); - - // Create a thread to handle server events: - tauri::async_runtime::spawn(async move { - info!("Start listening for server events..."); - loop { - match receiver.recv().await { - Some(ServerEvent::Started) => { - info!("The .NET server was started."); - }, - - Some(ServerEvent::NotFound(line)) => { - warn!("The .NET server issued a 404 error: {line}."); - }, - - Some(ServerEvent::Warning(line)) => { - warn!("The .NET server issued a warning: {line}."); - }, - - Some(ServerEvent::Error(line)) => { - error!("The .NET server issued an error: {line}."); - }, - - Some(ServerEvent::Stopped) => { - warn!("The .NET server was stopped."); - *server_receive_clone.lock().unwrap() = None; - }, - - None => { - debug!("Server event channel was closed."); - break; - }, + } else { + info!(Source = ".NET Server"; "{line}"); } } }); @@ -267,6 +276,19 @@ async fn main() { .setup(move |app| { let window = app.get_window("main").expect("Failed to get main window."); *MAIN_WINDOW.lock().unwrap() = Some(window); + + info!(Source = "Bootloader Tauri"; "Setup is running."); + let logger_path = app.path_resolver().app_local_data_dir().unwrap(); + let logger_path = logger_path.join("data"); + + info!(Source = "Bootloader Tauri"; "Reconfigure the file logger to use the app data directory {logger_path:?}"); + logger.reset_flw(&FileLogWriter::builder( + FileSpec::default() + .directory(logger_path) + .basename("events") + .suppress_timestamp() + .suffix("log")))?; + Ok(()) }) .plugin(tauri_plugin_window_state::Builder::default().build()) @@ -282,15 +304,15 @@ async fn main() { tauri::RunEvent::WindowEvent { event, label, .. } => { match event { tauri::WindowEvent::CloseRequested { .. } => { - warn!("Window '{label}': close was requested."); + warn!(Source = "Tauri"; "Window '{label}': close was requested."); } tauri::WindowEvent::Destroyed => { - warn!("Window '{label}': was destroyed."); + warn!(Source = "Tauri"; "Window '{label}': was destroyed."); } tauri::WindowEvent::FileDrop(files) => { - info!("Window '{label}': files were dropped: {files:?}"); + info!(Source = "Tauri"; "Window '{label}': files were dropped: {files:?}"); } _ => (), @@ -302,57 +324,136 @@ async fn main() { tauri::UpdaterEvent::UpdateAvailable { body, date, version } => { let body_len = body.len(); - info!("Updater: update available: body size={body_len} time={date:?} version={version}"); + info!(Source = "Tauri"; "Updater: update available: body size={body_len} time={date:?} version={version}"); } tauri::UpdaterEvent::Pending => { - info!("Updater: update is pending!"); + info!(Source = "Tauri"; "Updater: update is pending!"); } tauri::UpdaterEvent::DownloadProgress { chunk_length, content_length } => { - info!("Updater: downloaded {} of {:?}", chunk_length, content_length); + info!(Source = "Tauri"; "Updater: downloaded {} of {:?}", chunk_length, content_length); } tauri::UpdaterEvent::Downloaded => { - info!("Updater: update has been downloaded!"); - warn!("Try to stop the .NET server now..."); + info!(Source = "Tauri"; "Updater: update has been downloaded!"); + warn!(Source = "Tauri"; "Try to stop the .NET server now..."); stop_servers(); } tauri::UpdaterEvent::Updated => { - info!("Updater: app has been updated"); - warn!("Try to restart the app now..."); + info!(Source = "Tauri"; "Updater: app has been updated"); + warn!(Source = "Tauri"; "Try to restart the app now..."); app_handle.restart(); } tauri::UpdaterEvent::AlreadyUpToDate => { - info!("Updater: app is already up to date"); + info!(Source = "Tauri"; "Updater: app is already up to date"); } tauri::UpdaterEvent::Error(error) => { - warn!("Updater: failed to update: {error}"); + warn!(Source = "Tauri"; "Updater: failed to update: {error}"); } } } tauri::RunEvent::ExitRequested { .. } => { - warn!("Run event: exit was requested."); + warn!(Source = "Tauri"; "Run event: exit was requested."); } tauri::RunEvent::Ready => { - info!("Run event: Tauri app is ready."); + info!(Source = "Tauri"; "Run event: Tauri app is ready."); } _ => {} }); - info!("Tauri app was stopped."); + warn!(Source = "Tauri"; "Tauri app was stopped."); if is_prod() { - info!("Try to stop the .NET & runtime API servers as well..."); + warn!("Try to stop the .NET server as well..."); stop_servers(); } } +// +// Data structure for iterating over key-value pairs of log messages. +// +struct LogKVCollect<'kvs>(BTreeMap, Value<'kvs>>); + +impl<'kvs> VisitSource<'kvs> for LogKVCollect<'kvs> { + fn visit_pair(&mut self, key: Key<'kvs>, value: Value<'kvs>) -> Result<(), kv::Error> { + self.0.insert(key, value); + Ok(()) + } +} + +pub fn write_kv_pairs(w: &mut dyn std::io::Write, record: &log::Record) -> Result<(), std::io::Error> { + if record.key_values().count() > 0 { + let mut visitor = LogKVCollect(BTreeMap::new()); + record.key_values().visit(&mut visitor).unwrap(); + write!(w, "[")?; + let mut index = 0; + for (key, value) in visitor.0 { + index += 1; + if index > 1 { + write!(w, ", ")?; + } + + write!(w, "{} = {}", key, value)?; + } + write!(w, "] ")?; + } + + Ok(()) +} + +// Custom logger format for the terminal: +pub fn terminal_colored_logger_format( + w: &mut dyn std::io::Write, + now: &mut DeferredNow, + record: &log::Record, +) -> Result<(), std::io::Error> { + let level = record.level(); + + // Write the timestamp, log level, and module path: + write!( + w, + "[{}] {} [{}] ", + flexi_logger::style(level).paint(now.format(flexi_logger::TS_DASHES_BLANK_COLONS_DOT_BLANK).to_string()), + flexi_logger::style(level).paint(record.level().to_string()), + record.module_path().unwrap_or(""), + )?; + + // Write all key-value pairs: + write_kv_pairs(w, record)?; + + // Write the log message: + write!(w, "{}", flexi_logger::style(level).paint(record.args().to_string())) +} + +// Custom logger format for the log files: +pub fn file_logger_format( + w: &mut dyn std::io::Write, + now: &mut DeferredNow, + record: &log::Record, +) -> Result<(), std::io::Error> { + + // Write the timestamp, log level, and module path: + write!( + w, + "[{}] {} [{}] ", + now.format(flexi_logger::TS_DASHES_BLANK_COLONS_DOT_BLANK), + record.level(), + record.module_path().unwrap_or(""), + )?; + + // Write all key-value pairs: + write_kv_pairs(w, record)?; + + // Write the log message: + write!(w, "{}", &record.args()) +} + #[get("/system/dotnet/port")] fn dotnet_port() -> String { let dotnet_server_port = *DOTNET_SERVER_PORT; @@ -367,10 +468,11 @@ async fn dotnet_ready() { { Ok(url) => url, Err(msg) => { - error!("Error while parsing URL: {msg}"); + error!("Error while parsing URL for navigating to the app: {msg}"); return; } }; + info!("The .NET server was booted successfully."); // Try to get the main window. If it is not available yet, wait for it: @@ -397,20 +499,11 @@ async fn dotnet_ready() { let js_location_change = format!("window.location = '{url}';"); let location_change_result = main_window.as_ref().unwrap().eval(js_location_change.as_str()); match location_change_result { - Ok(_) => info!("Location was changed to {url}."), - Err(e) => error!("Failed to change location to {url}: {e}."), + Ok(_) => info!("The app location was changed to {url}."), + Err(e) => error!("Failed to change the app location to {url}: {e}."), } } -// Enum for server events: -enum ServerEvent { - Started, - NotFound(String), - Warning(String), - Error(String), - Stopped, -} - pub fn is_dev() -> bool { cfg!(debug_assertions) } @@ -447,7 +540,7 @@ async fn check_for_update() -> CheckUpdateResponse { true => { *CHECK_UPDATE_RESPONSE.lock().unwrap() = Some(update_response.clone()); let new_version = update_response.latest_version(); - info!("Updater: update to version '{new_version}' is available."); + info!(Source = "Updater"; "An update to version '{new_version}' is available."); let changelog = update_response.body(); CheckUpdateResponse { update_is_available: true, @@ -461,7 +554,7 @@ async fn check_for_update() -> CheckUpdateResponse { }, false => { - info!("Updater: no updates available."); + info!(Source = "Updater"; "No updates are available."); CheckUpdateResponse { update_is_available: false, error: false, @@ -472,7 +565,7 @@ async fn check_for_update() -> CheckUpdateResponse { }, Err(e) => { - warn!("Failed to check updater: {e}."); + warn!(Source = "Updater"; "Failed to check for updates: {e}."); CheckUpdateResponse { update_is_available: false, error: true, @@ -501,7 +594,7 @@ async fn install_update() { }, None => { - error!("Update installer: no update available to install. Did you check for updates first?"); + error!(Source = "Updater"; "No update available to install. Did you check for updates first?"); }, } } @@ -513,7 +606,7 @@ fn store_secret(destination: String, user_name: String, secret: String) -> Store let result = entry.set_password(secret.as_str()); match result { Ok(_) => { - info!("Secret for {service} and user {user_name} was stored successfully."); + info!(Source = "Secret Store"; "Secret for {service} and user {user_name} was stored successfully."); StoreSecretResponse { success: true, issue: String::from(""), @@ -521,7 +614,7 @@ fn store_secret(destination: String, user_name: String, secret: String) -> Store }, Err(e) => { - error!("Failed to store secret for {service} and user {user_name}: {e}."); + error!(Source = "Secret Store"; "Failed to store secret for {service} and user {user_name}: {e}."); StoreSecretResponse { success: false, issue: e.to_string(), @@ -543,7 +636,7 @@ fn get_secret(destination: String, user_name: String) -> RequestedSecret { let secret = entry.get_password(); match secret { Ok(s) => { - info!("Secret for {service} and user {user_name} was retrieved successfully."); + info!(Source = "Secret Store"; "Secret for {service} and user {user_name} was retrieved successfully."); RequestedSecret { success: true, secret: s, @@ -552,7 +645,7 @@ fn get_secret(destination: String, user_name: String) -> RequestedSecret { }, Err(e) => { - error!("Failed to retrieve secret for {service} and user {user_name}: {e}."); + error!(Source = "Secret Store"; "Failed to retrieve secret for {service} and user {user_name}: {e}."); RequestedSecret { success: false, secret: String::from(""), @@ -577,7 +670,7 @@ fn delete_secret(destination: String, user_name: String) -> DeleteSecretResponse match result { Ok(_) => { - warn!("Secret for {service} and user {user_name} was deleted successfully."); + warn!(Source = "Secret Store"; "Secret for {service} and user {user_name} was deleted successfully."); DeleteSecretResponse { success: true, was_entry_found: true, @@ -586,7 +679,7 @@ fn delete_secret(destination: String, user_name: String) -> DeleteSecretResponse }, Err(NoEntry) => { - warn!("No secret for {service} and user {user_name} was found."); + warn!(Source = "Secret Store"; "No secret for {service} and user {user_name} was found."); DeleteSecretResponse { success: true, was_entry_found: false, @@ -595,7 +688,7 @@ fn delete_secret(destination: String, user_name: String) -> DeleteSecretResponse } Err(e) => { - error!("Failed to delete secret for {service} and user {user_name}: {e}."); + error!(Source = "Secret Store"; "Failed to delete secret for {service} and user {user_name}: {e}."); DeleteSecretResponse { success: false, was_entry_found: false, @@ -618,7 +711,7 @@ fn set_clipboard(text: String) -> SetClipboardResponse { let mut clipboard = match clipboard_result { Ok(clipboard) => clipboard, Err(e) => { - error!("Failed to get the clipboard instance: {e}."); + error!(Source = "Clipboard"; "Failed to get the clipboard instance: {e}."); return SetClipboardResponse { success: false, issue: e.to_string(), @@ -629,7 +722,7 @@ fn set_clipboard(text: String) -> SetClipboardResponse { let set_text_result = clipboard.set_text(text); match set_text_result { Ok(_) => { - debug!("Text was set to the clipboard successfully."); + debug!(Source = "Clipboard"; "Text was set to the clipboard successfully."); SetClipboardResponse { success: true, issue: String::from(""), @@ -637,7 +730,7 @@ fn set_clipboard(text: String) -> SetClipboardResponse { }, Err(e) => { - error!("Failed to set text to the clipboard: {e}."); + error!(Source = "Clipboard"; "Failed to set text to the clipboard: {e}."); SetClipboardResponse { success: false, issue: e.to_string(),