Refactored logging into a unified logging

This commit is contained in:
Thorsten Sommer 2024-08-25 21:55:34 +02:00
parent 200b87f47a
commit 82f7329a39
Signed by: tsommer
GPG Key ID: 371BBA77A02C0108
24 changed files with 416 additions and 224 deletions

View File

@ -7,13 +7,15 @@ using AIStudio.Tools;
namespace AIStudio.Agents; namespace AIStudio.Agents;
public abstract class AgentBase(SettingsManager settingsManager, IJSRuntime jsRuntime, ThreadSafeRandom rng) : IAgent public abstract class AgentBase(ILogger<AgentBase> logger, SettingsManager settingsManager, IJSRuntime jsRuntime, ThreadSafeRandom rng) : IAgent
{ {
protected SettingsManager SettingsManager { get; init; } = settingsManager; protected SettingsManager SettingsManager { get; init; } = settingsManager;
protected IJSRuntime JsRuntime { get; init; } = jsRuntime; protected IJSRuntime JsRuntime { get; init; } = jsRuntime;
protected ThreadSafeRandom RNG { get; init; } = rng; protected ThreadSafeRandom RNG { get; init; } = rng;
protected ILogger<AgentBase> Logger { get; init; } = logger;
/// <summary> /// <summary>
/// Represents the type or category of this agent. /// 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. // Use the selected provider to get the AI response.
// By awaiting this line, we wait for the entire // By awaiting this line, we wait for the entire
// content to be streamed. // 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);
} }
} }

View File

@ -4,7 +4,7 @@ using AIStudio.Tools;
namespace AIStudio.Agents; namespace AIStudio.Agents;
public sealed class AgentTextContentCleaner(SettingsManager settingsManager, IJSRuntime jsRuntime, ThreadSafeRandom rng) : AgentBase(settingsManager, jsRuntime, rng) public sealed class AgentTextContentCleaner(ILogger<AgentBase> logger, SettingsManager settingsManager, IJSRuntime jsRuntime, ThreadSafeRandom rng) : AgentBase(logger, settingsManager, jsRuntime, rng)
{ {
private static readonly ContentBlock EMPTY_BLOCK = new() private static readonly ContentBlock EMPTY_BLOCK = new()
{ {

View File

@ -10,7 +10,7 @@ namespace AIStudio.Assistants;
public abstract partial class AssistantBase : ComponentBase public abstract partial class AssistantBase : ComponentBase
{ {
[Inject] [Inject]
protected SettingsManager SettingsManager { get; set; } = null!; protected SettingsManager SettingsManager { get; init; } = null!;
[Inject] [Inject]
protected IJSRuntime JsRuntime { get; init; } = null!; protected IJSRuntime JsRuntime { get; init; } = null!;
@ -27,6 +27,9 @@ public abstract partial class AssistantBase : ComponentBase
[Inject] [Inject]
protected NavigationManager NavigationManager { get; init; } = null!; protected NavigationManager NavigationManager { get; init; } = null!;
[Inject]
protected ILogger<AssistantBase> Logger { get; init; } = null!;
internal const string AFTER_RESULT_DIV_ID = "afterAssistantResult"; internal const string AFTER_RESULT_DIV_ID = "afterAssistantResult";
internal const string RESULT_DIV_ID = "assistantResult"; 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. // Use the selected provider to get the AI response.
// By awaiting this line, we wait for the entire // By awaiting this line, we wait for the entire
// content to be streamed. // 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.isProcessing = false;
this.StateHasChanged(); this.StateHasChanged();

View File

@ -24,6 +24,9 @@ public partial class Workspaces : ComponentBase
[Inject] [Inject]
private ThreadSafeRandom RNG { get; init; } = null!; private ThreadSafeRandom RNG { get; init; } = null!;
[Inject]
private ILogger<Workspaces> Logger { get; init; } = null!;
[Parameter] [Parameter]
public ChatThread? CurrentChatThread { get; set; } public ChatThread? CurrentChatThread { get; set; }
@ -309,7 +312,7 @@ public partial class Workspaces : ComponentBase
} }
catch (Exception e) catch (Exception e)
{ {
Console.WriteLine(e); this.Logger.LogError($"Failed to load chat from '{chatPath}': {e.Message}");
} }
return null; return null;

View File

@ -19,7 +19,7 @@ namespace AIStudio.Pages;
public partial class Chat : MSGComponentBase, IAsyncDisposable public partial class Chat : MSGComponentBase, IAsyncDisposable
{ {
[Inject] [Inject]
private SettingsManager SettingsManager { get; set; } = null!; private SettingsManager SettingsManager { get; init; } = null!;
[Inject] [Inject]
public IJSRuntime JsRuntime { get; init; } = null!; public IJSRuntime JsRuntime { get; init; } = null!;
@ -28,7 +28,10 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
private ThreadSafeRandom RNG { get; init; } = null!; private ThreadSafeRandom RNG { get; init; } = null!;
[Inject] [Inject]
public IDialogService DialogService { get; set; } = null!; private IDialogService DialogService { get; init; } = null!;
[Inject]
private ILogger<Chat> Logger { get; init; } = null!;
private InnerScrolling scrollingArea = null!; private InnerScrolling scrollingArea = null!;
@ -189,7 +192,7 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
// Use the selected provider to get the AI response. // Use the selected provider to get the AI response.
// By awaiting this line, we wait for the entire // By awaiting this line, we wait for the entire
// content to be streamed. // 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: // Save the chat:
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY) if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)

View File

@ -14,16 +14,19 @@ namespace AIStudio.Pages;
public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable
{ {
[Inject] [Inject]
public SettingsManager SettingsManager { get; init; } = null!; private SettingsManager SettingsManager { get; init; } = null!;
[Inject] [Inject]
public IDialogService DialogService { get; init; } = null!; private IDialogService DialogService { get; init; } = null!;
[Inject] [Inject]
public IJSRuntime JsRuntime { get; init; } = null!; private IJSRuntime JsRuntime { get; init; } = null!;
[Inject] [Inject]
protected MessageBus MessageBus { get; init; } = null!; private MessageBus MessageBus { get; init; } = null!;
[Inject]
private ILogger<Settings> Logger { get; init; } = null!;
private readonly List<ConfigurationSelectData<string>> availableProviders = new(); private readonly List<ConfigurationSelectData<string>> availableProviders = new();
@ -111,7 +114,7 @@ public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable
if (dialogResult is null || dialogResult.Canceled) if (dialogResult is null || dialogResult.Canceled)
return; return;
var providerInstance = provider.CreateProvider(); var providerInstance = provider.CreateProvider(this.Logger);
var deleteSecretResponse = await this.SettingsManager.DeleteAPIKey(this.JsRuntime, providerInstance); var deleteSecretResponse = await this.SettingsManager.DeleteAPIKey(this.JsRuntime, providerInstance);
if(deleteSecretResponse.Success) if(deleteSecretResponse.Success)
{ {

View File

@ -4,6 +4,8 @@ using AIStudio.Settings;
using AIStudio.Tools; using AIStudio.Tools;
using AIStudio.Tools.Services; using AIStudio.Tools.Services;
using Microsoft.Extensions.Logging.Console;
using MudBlazor.Services; using MudBlazor.Services;
#if !DEBUG #if !DEBUG
@ -35,6 +37,18 @@ if(string.IsNullOrWhiteSpace(secretKey))
} }
var builder = WebApplication.CreateBuilder(); 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<TerminalLogger, ConsoleFormatterOptions>();
builder.Services.AddMudServices(config => builder.Services.AddMudServices(config =>
{ {
config.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.BottomLeft; config.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.BottomLeft;
@ -101,5 +115,8 @@ app.MapRazorComponents<App>()
var serverTask = app.RunAsync(); var serverTask = app.RunAsync();
var rustLogger = app.Services.GetRequiredService<ILogger<Rust>>();
rust.SetLogger(rustLogger);
await rust.AppIsReady(); await rust.AppIsReady();
await serverTask; await serverTask;

View File

@ -8,7 +8,7 @@ using AIStudio.Settings;
namespace AIStudio.Provider.Anthropic; 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() private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
{ {

View File

@ -9,13 +9,21 @@ public abstract class BaseProvider
/// The HTTP client to use for all requests. /// The HTTP client to use for all requests.
/// </summary> /// </summary>
protected readonly HttpClient httpClient = new(); protected readonly HttpClient httpClient = new();
/// <summary>
/// The logger to use.
/// </summary>
protected readonly ILogger logger;
/// <summary> /// <summary>
/// Constructor for the base provider. /// Constructor for the base provider.
/// </summary> /// </summary>
/// <param name="url">The base URL for the provider.</param> /// <param name="url">The base URL for the provider.</param>
protected BaseProvider(string url) /// <param name="loggerService">The logger service to use.</param>
protected BaseProvider(string url, ILogger loggerService)
{ {
this.logger = loggerService;
// Set the base URL: // Set the base URL:
this.httpClient.BaseAddress = new(url); this.httpClient.BaseAddress = new(url);
} }

View File

@ -8,7 +8,7 @@ using AIStudio.Settings;
namespace AIStudio.Provider.Fireworks; 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() private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
{ {

View File

@ -9,7 +9,7 @@ using AIStudio.Settings;
namespace AIStudio.Provider.Mistral; 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() private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
{ {

View File

@ -11,7 +11,7 @@ namespace AIStudio.Provider.OpenAI;
/// <summary> /// <summary>
/// The OpenAI provider. /// The OpenAI provider.
/// </summary> /// </summary>
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() private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
{ {

View File

@ -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; namespace AIStudio.Provider;
/// <summary> /// <summary>
@ -20,59 +14,4 @@ public enum Providers
FIREWORKS = 5, FIREWORKS = 5,
SELF_HOSTED = 4, SELF_HOSTED = 4,
}
/// <summary>
/// Extension methods for the provider enum.
/// </summary>
public static class ExtensionsProvider
{
/// <summary>
/// Returns the human-readable name of the provider.
/// </summary>
/// <param name="provider">The provider.</param>
/// <returns>The human-readable name of the provider.</returns>
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",
};
/// <summary>
/// Creates a new provider instance based on the provider value.
/// </summary>
/// <param name="providerSettings">The provider settings.</param>
/// <returns>The provider instance.</returns>
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();
}
}
} }

View File

@ -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
{
/// <summary>
/// Returns the human-readable name of the provider.
/// </summary>
/// <param name="provider">The provider.</param>
/// <returns>The human-readable name of the provider.</returns>
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",
};
/// <summary>
/// Creates a new provider instance based on the provider value.
/// </summary>
/// <param name="providerSettings">The provider settings.</param>
/// <param name="logger">The logger to use.</param>
/// <returns>The provider instance.</returns>
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();
}
}
}

View File

@ -8,7 +8,7 @@ using AIStudio.Settings;
namespace AIStudio.Provider.SelfHosted; 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() private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
{ {
@ -162,7 +162,7 @@ public sealed class ProviderSelfHosted(Settings.Provider provider) : BaseProvide
} }
catch(Exception e) 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 []; return [];
} }
} }

View File

@ -71,10 +71,13 @@ public partial class ProviderDialog : ComponentBase
public bool IsEditing { get; init; } public bool IsEditing { get; init; }
[Inject] [Inject]
private SettingsManager SettingsManager { get; set; } = null!; private SettingsManager SettingsManager { get; init; } = null!;
[Inject] [Inject]
private IJSRuntime JsRuntime { get; set; } = null!; private IJSRuntime JsRuntime { get; init; } = null!;
[Inject]
private ILogger<ProviderDialog> Logger { get; init; } = null!;
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new(); private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
@ -133,7 +136,7 @@ public partial class ProviderDialog : ComponentBase
} }
var loadedProviderSettings = this.CreateProviderSettings(); var loadedProviderSettings = this.CreateProviderSettings();
var provider = loadedProviderSettings.CreateProvider(); var provider = loadedProviderSettings.CreateProvider(this.Logger);
if(provider is NoProvider) if(provider is NoProvider)
{ {
await base.OnInitializedAsync(); await base.OnInitializedAsync();
@ -187,7 +190,7 @@ public partial class ProviderDialog : ComponentBase
if (addedProviderSettings.UsedProvider != Providers.SELF_HOSTED) if (addedProviderSettings.UsedProvider != Providers.SELF_HOSTED)
{ {
// We need to instantiate the provider to store the API key: // 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: // Store the API key in the OS secure storage:
var storeResponse = await this.SettingsManager.SetAPIKey(this.JsRuntime, provider, this.dataAPIKey); var storeResponse = await this.SettingsManager.SetAPIKey(this.JsRuntime, provider, this.dataAPIKey);
@ -318,7 +321,7 @@ public partial class ProviderDialog : ComponentBase
private async Task ReloadModels() private async Task ReloadModels()
{ {
var currentProviderSettings = this.CreateProviderSettings(); var currentProviderSettings = this.CreateProviderSettings();
var provider = currentProviderSettings.CreateProvider(); var provider = currentProviderSettings.CreateProvider(this.Logger);
if(provider is NoProvider) if(provider is NoProvider)
return; return;

View File

@ -11,7 +11,7 @@ namespace AIStudio.Settings;
/// <summary> /// <summary>
/// The settings manager. /// The settings manager.
/// </summary> /// </summary>
public sealed class SettingsManager public sealed class SettingsManager(ILogger<SettingsManager> logger)
{ {
private const string SETTINGS_FILENAME = "settings.json"; private const string SETTINGS_FILENAME = "settings.json";
@ -20,6 +20,8 @@ public sealed class SettingsManager
WriteIndented = true, WriteIndented = true,
Converters = { new JsonStringEnumConverter() }, Converters = { new JsonStringEnumConverter() },
}; };
private ILogger<SettingsManager> logger = logger;
/// <summary> /// <summary>
/// The directory where the configuration files are stored. /// The directory where the configuration files are stored.
@ -102,12 +104,18 @@ public sealed class SettingsManager
public async Task LoadSettings() public async Task LoadSettings()
{ {
if(!this.IsSetUp) if(!this.IsSetUp)
{
this.logger.LogWarning("Cannot load settings, because the configuration is not set up yet.");
return; return;
}
var settingsPath = Path.Combine(ConfigDirectory!, SETTINGS_FILENAME); var settingsPath = Path.Combine(ConfigDirectory!, SETTINGS_FILENAME);
if(!File.Exists(settingsPath)) if(!File.Exists(settingsPath))
{
this.logger.LogWarning("Cannot load settings, because the settings file does not exist.");
return; return;
}
// We read the `"Version": "V3"` line to determine the version of the settings file: // We read the `"Version": "V3"` line to determine the version of the settings file:
await foreach (var line in File.ReadLinesAsync(settingsPath)) await foreach (var line in File.ReadLinesAsync(settingsPath))
{ {
@ -123,16 +131,16 @@ public sealed class SettingsManager
Enum.TryParse(settingsVersionText, out Version settingsVersion); Enum.TryParse(settingsVersionText, out Version settingsVersion);
if(settingsVersion is Version.UNKNOWN) 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(); this.ConfigurationData = new();
return; 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; 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(); this.ConfigurationData = new();
} }
@ -142,14 +150,22 @@ public sealed class SettingsManager
public async Task StoreSettings() public async Task StoreSettings()
{ {
if(!this.IsSetUp) if(!this.IsSetUp)
{
this.logger.LogWarning("Cannot store settings, because the configuration is not set up yet.");
return; return;
}
var settingsPath = Path.Combine(ConfigDirectory!, SETTINGS_FILENAME); var settingsPath = Path.Combine(ConfigDirectory!, SETTINGS_FILENAME);
if(!Directory.Exists(ConfigDirectory)) if(!Directory.Exists(ConfigDirectory))
{
this.logger.LogInformation("Creating the configuration directory.");
Directory.CreateDirectory(ConfigDirectory!); Directory.CreateDirectory(ConfigDirectory!);
}
var settingsJson = JsonSerializer.Serialize(this.ConfigurationData, JSON_OPTIONS); var settingsJson = JsonSerializer.Serialize(this.ConfigurationData, JSON_OPTIONS);
await File.WriteAllTextAsync(settingsPath, settingsJson); await File.WriteAllTextAsync(settingsPath, settingsJson);
this.logger.LogInformation("Stored the settings to the file system.");
} }
public void InjectSpellchecking(Dictionary<string, object?> attributes) => attributes["spellcheck"] = this.ConfigurationData.App.EnableSpellchecking ? "true" : "false"; public void InjectSpellchecking(Dictionary<string, object?> attributes) => attributes["spellcheck"] = this.ConfigurationData.App.EnableSpellchecking ? "true" : "false";

View File

@ -9,7 +9,7 @@ namespace AIStudio.Settings;
public static class SettingsMigrations public static class SettingsMigrations
{ {
public static Data Migrate(Version previousVersion, string configData, JsonSerializerOptions jsonOptions) public static Data Migrate(ILogger<SettingsManager> logger, Version previousVersion, string configData, JsonSerializerOptions jsonOptions)
{ {
switch (previousVersion) switch (previousVersion)
{ {
@ -17,41 +17,41 @@ public static class SettingsMigrations
var configV1 = JsonSerializer.Deserialize<DataV1V3>(configData, jsonOptions); var configV1 = JsonSerializer.Deserialize<DataV1V3>(configData, jsonOptions);
if (configV1 is null) 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(); return new();
} }
configV1 = MigrateV1ToV2(configV1); configV1 = MigrateV1ToV2(logger, configV1);
configV1 = MigrateV2ToV3(configV1); configV1 = MigrateV2ToV3(logger, configV1);
return MigrateV3ToV4(configV1); return MigrateV3ToV4(logger, configV1);
case Version.V2: case Version.V2:
var configV2 = JsonSerializer.Deserialize<DataV1V3>(configData, jsonOptions); var configV2 = JsonSerializer.Deserialize<DataV1V3>(configData, jsonOptions);
if (configV2 is null) 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(); return new();
} }
configV2 = MigrateV2ToV3(configV2); configV2 = MigrateV2ToV3(logger, configV2);
return MigrateV3ToV4(configV2); return MigrateV3ToV4(logger, configV2);
case Version.V3: case Version.V3:
var configV3 = JsonSerializer.Deserialize<DataV1V3>(configData, jsonOptions); var configV3 = JsonSerializer.Deserialize<DataV1V3>(configData, jsonOptions);
if (configV3 is null) 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 new();
} }
return MigrateV3ToV4(configV3); return MigrateV3ToV4(logger, configV3);
default: default:
Console.WriteLine("No configuration migration needed."); logger.LogInformation("No configuration migration is needed.");
var configV4 = JsonSerializer.Deserialize<Data>(configData, jsonOptions); var configV4 = JsonSerializer.Deserialize<Data>(configData, jsonOptions);
if (configV4 is null) 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(); return new();
} }
@ -59,14 +59,14 @@ public static class SettingsMigrations
} }
} }
private static DataV1V3 MigrateV1ToV2(DataV1V3 previousData) private static DataV1V3 MigrateV1ToV2(ILogger<SettingsManager> logger, DataV1V3 previousData)
{ {
// //
// Summary: // Summary:
// In v1 we had no self-hosted providers. Thus, we had no hostnames. // 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() return new()
{ {
Version = Version.V2, Version = Version.V2,
@ -81,14 +81,14 @@ public static class SettingsMigrations
}; };
} }
private static DataV1V3 MigrateV2ToV3(DataV1V3 previousData) private static DataV1V3 MigrateV2ToV3(ILogger<SettingsManager> logger, DataV1V3 previousData)
{ {
// //
// Summary: // Summary:
// In v2, self-hosted providers had no host (LM Studio, llama.cpp, ollama, etc.) // 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() return new()
{ {
Version = Version.V3, Version = Version.V3,
@ -110,14 +110,14 @@ public static class SettingsMigrations
}; };
} }
private static Data MigrateV3ToV4(DataV1V3 previousConfig) private static Data MigrateV3ToV4(ILogger<SettingsManager> logger, DataV1V3 previousConfig)
{ {
// //
// Summary: // Summary:
// We grouped the settings into different categories. // We grouped the settings into different categories.
// //
Console.WriteLine("Migrating from v3 to v4..."); logger.LogInformation("Migrating from v3 to v4...");
return new() return new()
{ {
Version = Version.V4, Version = Version.V4,

View File

@ -9,6 +9,13 @@ public sealed class Rust(string apiPort) : IDisposable
{ {
BaseAddress = new Uri($"http://127.0.0.1:{apiPort}"), BaseAddress = new Uri($"http://127.0.0.1:{apiPort}"),
}; };
private ILogger<Rust>? logger;
public void SetLogger(ILogger<Rust> logService)
{
this.logger = logService;
}
public async Task<int> GetAppPort() public async Task<int> GetAppPort()
{ {
@ -36,14 +43,14 @@ public sealed class Rust(string apiPort) : IDisposable
var response = await initialHttp.GetAsync(url); var response = await initialHttp.GetAsync(url);
if (!response.IsSuccessStatusCode) 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); await Task.Delay(wait4Try);
continue; continue;
} }
var appPortContent = await response.Content.ReadAsStringAsync(); var appPortContent = await response.Content.ReadAsStringAsync();
var appPort = int.Parse(appPortContent); 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; return appPort;
} }
@ -54,11 +61,11 @@ public sealed class Rust(string apiPort) : IDisposable
public async Task AppIsReady() public async Task AppIsReady()
{ {
const string URL = "/system/dotnet/ready"; 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)); var response = await this.http.PostAsync(URL, new StringContent(string.Empty));
if (!response.IsSuccessStatusCode) 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}'");
} }
} }

View File

@ -15,7 +15,7 @@ public sealed class MarkdownClipboardService(Rust rust, IJSRuntime jsRuntime, IS
private Rust Rust { get; } = rust; private Rust Rust { get; } = rust;
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
/// <param name="text">The Markdown text to copy.</param> /// <param name="text">The Markdown text to copy.</param>
public async ValueTask CopyToClipboardAsync(string text) => await this.Rust.CopyText2Clipboard(this.JsRuntime, this.Snackbar, text); public async ValueTask CopyToClipboardAsync(string text) => await this.Rust.CopyText2Clipboard(this.JsRuntime, this.Snackbar, text);

View File

@ -3,11 +3,13 @@ using AIStudio.Settings.DataModel;
namespace AIStudio.Tools.Services; namespace AIStudio.Tools.Services;
public class TemporaryChatService(SettingsManager settingsManager) : BackgroundService public class TemporaryChatService(ILogger<TemporaryChatService> logger, SettingsManager settingsManager) : BackgroundService
{ {
private static readonly TimeSpan CHECK_INTERVAL = TimeSpan.FromDays(1); private static readonly TimeSpan CHECK_INTERVAL = TimeSpan.FromDays(1);
private static bool IS_INITIALIZED; private static bool IS_INITIALIZED;
private readonly ILogger<TemporaryChatService> logger = logger;
#region Overrides of BackgroundService #region Overrides of BackgroundService
protected override async Task ExecuteAsync(CancellationToken stoppingToken) protected override async Task ExecuteAsync(CancellationToken stoppingToken)
@ -15,10 +17,12 @@ public class TemporaryChatService(SettingsManager settingsManager) : BackgroundS
while (!stoppingToken.IsCancellationRequested && !IS_INITIALIZED) while (!stoppingToken.IsCancellationRequested && !IS_INITIALIZED)
await Task.Delay(TimeSpan.FromSeconds(3), stoppingToken); await Task.Delay(TimeSpan.FromSeconds(3), stoppingToken);
this.logger.LogInformation("The temporary chat maintenance service was initialized.");
await settingsManager.LoadSettings(); await settingsManager.LoadSettings();
if(settingsManager.ConfigurationData.Workspace.StorageTemporaryMaintenancePolicy is WorkspaceStorageTemporaryMaintenancePolicy.NO_AUTOMATIC_MAINTENANCE) 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; return;
} }
@ -34,10 +38,14 @@ public class TemporaryChatService(SettingsManager settingsManager) : BackgroundS
private Task StartMaintenance() private Task StartMaintenance()
{ {
this.logger.LogInformation("Starting maintenance of temporary chat storage.");
var temporaryDirectories = Path.Join(SettingsManager.DataDirectory, "tempChats"); var temporaryDirectories = Path.Join(SettingsManager.DataDirectory, "tempChats");
if(!Directory.Exists(temporaryDirectories)) if(!Directory.Exists(temporaryDirectories))
{
this.logger.LogWarning("Temporary chat storage directory does not exist. End maintenance.");
return Task.CompletedTask; return Task.CompletedTask;
}
foreach (var tempChatDirPath in Directory.EnumerateDirectories(temporaryDirectories)) foreach (var tempChatDirPath in Directory.EnumerateDirectories(temporaryDirectories))
{ {
var chatPath = Path.Join(tempChatDirPath, "thread.json"); var chatPath = Path.Join(tempChatDirPath, "thread.json");
@ -59,9 +67,13 @@ public class TemporaryChatService(SettingsManager settingsManager) : BackgroundS
}; };
if(deleteChat) if(deleteChat)
{
this.logger.LogInformation($"Deleting temporary chat storage directory '{tempChatDirPath}' due to maintenance policy.");
Directory.Delete(tempChatDirPath, true); Directory.Delete(tempChatDirPath, true);
}
} }
this.logger.LogInformation("Finished maintenance of temporary chat storage.");
return Task.CompletedTask; return Task.CompletedTask;
} }

View File

@ -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<TState>(in LogEntry<TState> 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();
}
}

View File

@ -17,7 +17,7 @@ keyring = { version = "3.2", features = ["apple-native", "windows-native", "sync
arboard = "3.4.0" arboard = "3.4.0"
tokio = { version = "1.39", features = ["rt", "rt-multi-thread", "macros"] } tokio = { version = "1.39", features = ["rt", "rt-multi-thread", "macros"] }
flexi_logger = "0.28" flexi_logger = "0.28"
log = "0.4" log = { version = "0.4", features = ["kv"] }
once_cell = "1.19.0" once_cell = "1.19.0"
rocket = { version = "0.5", default-features = false, features = ["json"] } rocket = { version = "0.5", default-features = false, features = ["json"] }
rand = "0.8" rand = "0.8"

View File

@ -4,9 +4,11 @@
extern crate rocket; extern crate rocket;
extern crate core; extern crate core;
use std::collections::HashSet; use std::collections::{BTreeMap, HashMap, HashSet};
use std::iter::once;
use std::net::TcpListener; use std::net::TcpListener;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::fmt::Write;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use arboard::Clipboard; use arboard::Clipboard;
@ -15,9 +17,12 @@ use serde::Serialize;
use tauri::{Manager, Url, Window}; use tauri::{Manager, Url, Window};
use tauri::api::process::{Command, CommandChild, CommandEvent}; use tauri::api::process::{Command, CommandChild, CommandEvent};
use tokio::time; 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 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::figment::Figment;
use rocket::{get, post, routes}; use rocket::{get, post, routes};
use rocket::config::Shutdown; use rocket::config::Shutdown;
@ -65,16 +70,36 @@ async fn main() {
let tauri_version = metadata_lines.next().unwrap(); let tauri_version = metadata_lines.next().unwrap();
let app_commit_hash = 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. // Configure the logger:
let log_level = match is_dev() { //
true => "debug", let mut log_config = String::new();
false => "info",
// 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") // Set the log level for the Rocket library:
.log_to_stdout() log_config.push_str("rocket=info, ");
.adaptive_format_for_stdout(AdaptiveFormat::Detailed)
// 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"); .start().expect("Cannot start logging");
info!("Starting MindWork AI Studio:"); info!("Starting MindWork AI Studio:");
@ -94,6 +119,8 @@ async fn main() {
let api_port = *API_SERVER_PORT; let api_port = *API_SERVER_PORT;
info!("Try to start the API server on 'http://localhost:{api_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()) let figment = Figment::from(rocket::Config::release_default())
// We use the next available port which was determined before: // We use the next available port which was determined before:
@ -112,6 +139,9 @@ async fn main() {
.merge(("workers", 3)) .merge(("workers", 3))
.merge(("max_blocking", 12)) .merge(("max_blocking", 12))
// No colors and emojis in the log output:
.merge(("cli_colors", false))
// Set the shutdown configuration: // Set the shutdown configuration:
.merge(("shutdown", Shutdown { .merge(("shutdown", Shutdown {
@ -125,6 +155,7 @@ async fn main() {
..Shutdown::default() ..Shutdown::default()
})); }));
//
// Start the runtime API server in a separate thread. This is necessary // 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 // because the server is blocking, and we need to run the Tauri app in
// parallel: // parallel:
@ -134,6 +165,7 @@ async fn main() {
.mount("/", routes![dotnet_port, dotnet_ready]) .mount("/", routes![dotnet_port, dotnet_ready])
.ignite().await.unwrap() .ignite().await.unwrap()
.launch().await.unwrap(); .launch().await.unwrap();
});
// //
// Generate a secret key for the AES encryption for the IPC channel: // 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: // Log the output of the .NET server:
while let Some(CommandEvent::Stdout(line)) = rx.recv().await { 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, // Remove newline characters from the end:
_ if line_cleared.contains("warn") => _ = sender.send(ServerEvent::Warning(line)).await, let line = line.trim_end();
_ if line_cleared.contains("404") => _ = sender.send(ServerEvent::NotFound(line)).await,
_ => (), // 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: '<YYYY-MM-dd HH:mm:ss.fff> [<log level>] <source>: <message>'.
// 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}')"),
} }
} } else {
info!(Source = ".NET Server"; "{line}");
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;
},
} }
} }
}); });
@ -267,6 +276,19 @@ async fn main() {
.setup(move |app| { .setup(move |app| {
let window = app.get_window("main").expect("Failed to get main window."); let window = app.get_window("main").expect("Failed to get main window.");
*MAIN_WINDOW.lock().unwrap() = Some(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(()) Ok(())
}) })
.plugin(tauri_plugin_window_state::Builder::default().build()) .plugin(tauri_plugin_window_state::Builder::default().build())
@ -282,15 +304,15 @@ async fn main() {
tauri::RunEvent::WindowEvent { event, label, .. } => { tauri::RunEvent::WindowEvent { event, label, .. } => {
match event { match event {
tauri::WindowEvent::CloseRequested { .. } => { tauri::WindowEvent::CloseRequested { .. } => {
warn!("Window '{label}': close was requested."); warn!(Source = "Tauri"; "Window '{label}': close was requested.");
} }
tauri::WindowEvent::Destroyed => { tauri::WindowEvent::Destroyed => {
warn!("Window '{label}': was destroyed."); warn!(Source = "Tauri"; "Window '{label}': was destroyed.");
} }
tauri::WindowEvent::FileDrop(files) => { 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 } => { tauri::UpdaterEvent::UpdateAvailable { body, date, version } => {
let body_len = body.len(); 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 => { tauri::UpdaterEvent::Pending => {
info!("Updater: update is pending!"); info!(Source = "Tauri"; "Updater: update is pending!");
} }
tauri::UpdaterEvent::DownloadProgress { chunk_length, content_length } => { 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 => { tauri::UpdaterEvent::Downloaded => {
info!("Updater: update has been downloaded!"); info!(Source = "Tauri"; "Updater: update has been downloaded!");
warn!("Try to stop the .NET server now..."); warn!(Source = "Tauri"; "Try to stop the .NET server now...");
stop_servers(); stop_servers();
} }
tauri::UpdaterEvent::Updated => { tauri::UpdaterEvent::Updated => {
info!("Updater: app has been updated"); info!(Source = "Tauri"; "Updater: app has been updated");
warn!("Try to restart the app now..."); warn!(Source = "Tauri"; "Try to restart the app now...");
app_handle.restart(); app_handle.restart();
} }
tauri::UpdaterEvent::AlreadyUpToDate => { 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) => { tauri::UpdaterEvent::Error(error) => {
warn!("Updater: failed to update: {error}"); warn!(Source = "Tauri"; "Updater: failed to update: {error}");
} }
} }
} }
tauri::RunEvent::ExitRequested { .. } => { tauri::RunEvent::ExitRequested { .. } => {
warn!("Run event: exit was requested."); warn!(Source = "Tauri"; "Run event: exit was requested.");
} }
tauri::RunEvent::Ready => { 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() { 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(); stop_servers();
} }
} }
//
// Data structure for iterating over key-value pairs of log messages.
//
struct LogKVCollect<'kvs>(BTreeMap<Key<'kvs>, 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("<unnamed>"),
)?;
// 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("<unnamed>"),
)?;
// Write all key-value pairs:
write_kv_pairs(w, record)?;
// Write the log message:
write!(w, "{}", &record.args())
}
#[get("/system/dotnet/port")] #[get("/system/dotnet/port")]
fn dotnet_port() -> String { fn dotnet_port() -> String {
let dotnet_server_port = *DOTNET_SERVER_PORT; let dotnet_server_port = *DOTNET_SERVER_PORT;
@ -367,10 +468,11 @@ async fn dotnet_ready() {
{ {
Ok(url) => url, Ok(url) => url,
Err(msg) => { Err(msg) => {
error!("Error while parsing URL: {msg}"); error!("Error while parsing URL for navigating to the app: {msg}");
return; return;
} }
}; };
info!("The .NET server was booted successfully."); info!("The .NET server was booted successfully.");
// Try to get the main window. If it is not available yet, wait for it: // 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 js_location_change = format!("window.location = '{url}';");
let location_change_result = main_window.as_ref().unwrap().eval(js_location_change.as_str()); let location_change_result = main_window.as_ref().unwrap().eval(js_location_change.as_str());
match location_change_result { match location_change_result {
Ok(_) => info!("Location was changed to {url}."), Ok(_) => info!("The app location was changed to {url}."),
Err(e) => error!("Failed to change location to {url}: {e}."), 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 { pub fn is_dev() -> bool {
cfg!(debug_assertions) cfg!(debug_assertions)
} }
@ -447,7 +540,7 @@ async fn check_for_update() -> CheckUpdateResponse {
true => { true => {
*CHECK_UPDATE_RESPONSE.lock().unwrap() = Some(update_response.clone()); *CHECK_UPDATE_RESPONSE.lock().unwrap() = Some(update_response.clone());
let new_version = update_response.latest_version(); 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(); let changelog = update_response.body();
CheckUpdateResponse { CheckUpdateResponse {
update_is_available: true, update_is_available: true,
@ -461,7 +554,7 @@ async fn check_for_update() -> CheckUpdateResponse {
}, },
false => { false => {
info!("Updater: no updates available."); info!(Source = "Updater"; "No updates are available.");
CheckUpdateResponse { CheckUpdateResponse {
update_is_available: false, update_is_available: false,
error: false, error: false,
@ -472,7 +565,7 @@ async fn check_for_update() -> CheckUpdateResponse {
}, },
Err(e) => { Err(e) => {
warn!("Failed to check updater: {e}."); warn!(Source = "Updater"; "Failed to check for updates: {e}.");
CheckUpdateResponse { CheckUpdateResponse {
update_is_available: false, update_is_available: false,
error: true, error: true,
@ -501,7 +594,7 @@ async fn install_update() {
}, },
None => { 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()); let result = entry.set_password(secret.as_str());
match result { match result {
Ok(_) => { 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 { StoreSecretResponse {
success: true, success: true,
issue: String::from(""), issue: String::from(""),
@ -521,7 +614,7 @@ fn store_secret(destination: String, user_name: String, secret: String) -> Store
}, },
Err(e) => { 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 { StoreSecretResponse {
success: false, success: false,
issue: e.to_string(), issue: e.to_string(),
@ -543,7 +636,7 @@ fn get_secret(destination: String, user_name: String) -> RequestedSecret {
let secret = entry.get_password(); let secret = entry.get_password();
match secret { match secret {
Ok(s) => { 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 { RequestedSecret {
success: true, success: true,
secret: s, secret: s,
@ -552,7 +645,7 @@ fn get_secret(destination: String, user_name: String) -> RequestedSecret {
}, },
Err(e) => { 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 { RequestedSecret {
success: false, success: false,
secret: String::from(""), secret: String::from(""),
@ -577,7 +670,7 @@ fn delete_secret(destination: String, user_name: String) -> DeleteSecretResponse
match result { match result {
Ok(_) => { 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 { DeleteSecretResponse {
success: true, success: true,
was_entry_found: true, was_entry_found: true,
@ -586,7 +679,7 @@ fn delete_secret(destination: String, user_name: String) -> DeleteSecretResponse
}, },
Err(NoEntry) => { 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 { DeleteSecretResponse {
success: true, success: true,
was_entry_found: false, was_entry_found: false,
@ -595,7 +688,7 @@ fn delete_secret(destination: String, user_name: String) -> DeleteSecretResponse
} }
Err(e) => { 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 { DeleteSecretResponse {
success: false, success: false,
was_entry_found: false, was_entry_found: false,
@ -618,7 +711,7 @@ fn set_clipboard(text: String) -> SetClipboardResponse {
let mut clipboard = match clipboard_result { let mut clipboard = match clipboard_result {
Ok(clipboard) => clipboard, Ok(clipboard) => clipboard,
Err(e) => { Err(e) => {
error!("Failed to get the clipboard instance: {e}."); error!(Source = "Clipboard"; "Failed to get the clipboard instance: {e}.");
return SetClipboardResponse { return SetClipboardResponse {
success: false, success: false,
issue: e.to_string(), issue: e.to_string(),
@ -629,7 +722,7 @@ fn set_clipboard(text: String) -> SetClipboardResponse {
let set_text_result = clipboard.set_text(text); let set_text_result = clipboard.set_text(text);
match set_text_result { match set_text_result {
Ok(_) => { Ok(_) => {
debug!("Text was set to the clipboard successfully."); debug!(Source = "Clipboard"; "Text was set to the clipboard successfully.");
SetClipboardResponse { SetClipboardResponse {
success: true, success: true,
issue: String::from(""), issue: String::from(""),
@ -637,7 +730,7 @@ fn set_clipboard(text: String) -> SetClipboardResponse {
}, },
Err(e) => { Err(e) => {
error!("Failed to set text to the clipboard: {e}."); error!(Source = "Clipboard"; "Failed to set text to the clipboard: {e}.");
SetClipboardResponse { SetClipboardResponse {
success: false, success: false,
issue: e.to_string(), issue: e.to_string(),