diff --git a/CHANGELOG.md b/CHANGELOG.md index d2a63f385d..d43113c056 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,29 @@ ## vNext (TBD) +### Deprecations +* The `Logger` has been deprecated in favor of `RealmLogger`, which `Logger` currently derives from. (PR [#3634](https://github.com/realm/realm-dotnet/pull/3634)) + * The `Logger.LogLevel` `set` and `get` accessors have been deprecated. Please use `RealmLogger.SetLogLevel()` and `RealmLogger.GetLogLevel()` (see **Enhancements** below). + * The `Logger.Function(Action logFunction)` have been deprecated. Please use `RealmLogger.Function(Action logFunction)` (see **Enhancements** below). + ### Enhancements * Allow `ShouldCompactOnLaunch` to be set on `SyncConfiguration`, not only `RealmConfiguration`. (Issue [#3617](https://github.com/realm/realm-dotnet/issues/3617)) * Reduce the size of the local transaction log produced by creating objects, improving the performance of insertion-heavy transactions (Core 14.10.0). * Performance has been improved for range queries on integers and timestamps. Requires that you use the "BETWEEN" operation in `Realm.All().Filter(...)`. (Core 14.10.1) +* Allowed `ShouldCompactOnLaunch` to be set on `SyncConfiguration`, not only `RealmConfiguration`. (Issue [#3617](https://github.com/realm/realm-dotnet/issues/3617)) +* Introduced a `LogCategory` and allowed for more control over which category of messages should be logged and at which criticality level. (PR [#3634](https://github.com/realm/realm-dotnet/pull/3634)) + * Allowed setting and getting a `LogLevel` for a given `LogCategory`. The hierarchy of categories starts at `LogCategory.Realm`. + ```csharp + RealmLogger.SetLogLevel(LogLevel.Warn, LogCategory.Realm.Sync); + RealmLogger.GetLogLevel(LogCategory.Realm.Sync.Client.Session); // LogLevel.Warn + ``` + * Added a function logger that accepts a callback that will receive the `LogLevel`, `LogCategory`, and the message when invoked. + ```csharp + RealmLogger.Default = RealmLogger.Function((level, category, message) => /* custom implementation */); + ``` + * Added a `RealmLogger.Log()` overload taking a category. The pre-existing `Log()` API will implicitly log at `LogCategory.Realm.SDK`. + ```csharp + RealmLogger.Default.Log(LogLevel.Warn, LogCategory.Realm, "A warning message"); + ``` ### Fixed * A `ForCurrentlyOutstandingWork` progress notifier would not immediately call its callback after registration. Instead you would have to wait for some data to be received to get your first update - if you were already caught up when you registered the notifier you could end up waiting a long time for the server to deliver a download that would call/expire your notifier. (Core 14.8.0) diff --git a/Realm/Realm.UnityUtils/Initializer.cs b/Realm/Realm.UnityUtils/Initializer.cs index e7f317b93f..f8dd575389 100644 --- a/Realm/Realm.UnityUtils/Initializer.cs +++ b/Realm/Realm.UnityUtils/Initializer.cs @@ -37,7 +37,7 @@ public static void Initialize() Platform.DeviceInfo = new UnityDeviceInfo(); Platform.BundleId = Application.productName; InteropConfig.AddPotentialStorageFolder(FileHelper.GetStorageFolder()); - Realms.Logging.Logger.Console = new UnityLogger(); + Realms.Logging.RealmLogger.Console = new UnityLogger(); Application.quitting += () => { NativeCommon.CleanupNativeResources("Application is exiting"); diff --git a/Realm/Realm.UnityUtils/UnityLogger.cs b/Realm/Realm.UnityUtils/UnityLogger.cs index f580f35a01..4f2713179c 100644 --- a/Realm/Realm.UnityUtils/UnityLogger.cs +++ b/Realm/Realm.UnityUtils/UnityLogger.cs @@ -20,11 +20,14 @@ namespace UnityUtils { - public class UnityLogger : Logger + /// + /// A that outputs messages via UnityEngine. + /// + public class UnityLogger : RealmLogger { - protected override void LogImpl(LogLevel level, string message) + protected override void LogImpl(LogLevel level, LogCategory category, string message) { - var toLog = FormatLog(level, message); + var toLog = FormatLog(level, category!, message); switch (level) { case LogLevel.Fatal: diff --git a/Realm/Realm/Handles/RealmHandle.cs b/Realm/Realm/Handles/RealmHandle.cs index 6aa61ede0f..02c5b7a7aa 100644 --- a/Realm/Realm/Handles/RealmHandle.cs +++ b/Realm/Realm/Handles/RealmHandle.cs @@ -148,7 +148,7 @@ protected override bool ReleaseHandle() } catch(Exception ex) { - Logger.Default.Log(LogLevel.Error, $"An error occurred while closing native handle. Please file an issue at https://github.com/realm/realm-dotnet/issues. Error: {ex}"); + RealmLogger.Default.Log(LogLevel.Error, $"An error occurred while closing native handle. Please file an issue at https://github.com/realm/realm-dotnet/issues. Error: {ex}"); Debug.Fail($"Failed to close native handle: {ex}"); // it would be really bad if we got an exception in here. We must not pass it on, but have to return false diff --git a/Realm/Realm/Handles/SessionHandle.cs b/Realm/Realm/Handles/SessionHandle.cs index 6068ce6102..060f5daf9a 100644 --- a/Realm/Realm/Handles/SessionHandle.cs +++ b/Realm/Realm/Handles/SessionHandle.cs @@ -325,7 +325,7 @@ private static void HandleSessionError(IntPtr sessionHandlePtr, SyncError error, } catch (Exception ex) { - Logger.Default.Log(LogLevel.Warn, $"An error has occurred while handling a session error: {ex}"); + RealmLogger.Default.Log(LogLevel.Warn, $"An error has occurred while handling a session error: {ex}"); } } @@ -359,7 +359,7 @@ private static IntPtr NotifyBeforeClientReset(IntPtr beforeFrozen, IntPtr manage catch (Exception ex) { var handlerType = syncConfig is null ? "ClientResetHandler" : syncConfig.ClientResetHandler.GetType().Name; - Logger.Default.Log(LogLevel.Error, $"An error has occurred while executing {handlerType}.OnBeforeReset during a client reset: {ex}"); + RealmLogger.Default.Log(LogLevel.Error, $"An error has occurred while executing {handlerType}.OnBeforeReset during a client reset: {ex}"); var exHandle = GCHandle.Alloc(ex); return GCHandle.ToIntPtr(exHandle); @@ -397,7 +397,7 @@ private static IntPtr NotifyAfterClientReset(IntPtr beforeFrozen, IntPtr after, catch (Exception ex) { var handlerType = syncConfig is null ? "ClientResetHandler" : syncConfig.ClientResetHandler.GetType().Name; - Logger.Default.Log(LogLevel.Error, $"An error has occurred while executing {handlerType}.OnAfterReset during a client reset: {ex}"); + RealmLogger.Default.Log(LogLevel.Error, $"An error has occurred while executing {handlerType}.OnAfterReset during a client reset: {ex}"); var exHandle = GCHandle.Alloc(ex); return GCHandle.ToIntPtr(exHandle); @@ -460,7 +460,7 @@ private static void HandleSessionPropertyChangedCallback(IntPtr managedSessionHa } catch (Exception ex) { - Logger.Default.Log(LogLevel.Error, $"An error has occurred while raising a property changed event: {ex}"); + RealmLogger.Default.Log(LogLevel.Error, $"An error has occurred while raising a property changed event: {ex}"); } } diff --git a/Realm/Realm/Handles/SharedRealmHandle.cs b/Realm/Realm/Handles/SharedRealmHandle.cs index 9b438f2247..17962d92b2 100644 --- a/Realm/Realm/Handles/SharedRealmHandle.cs +++ b/Realm/Realm/Handles/SharedRealmHandle.cs @@ -19,6 +19,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; @@ -48,6 +49,17 @@ internal class SharedRealmHandle : StandaloneHandle private static class NativeMethods { + // This is a wrapper struct around MarshaledVector since P/Invoke doesn't like it + // when the MarshaledVector is returned as the top-level return value from a native + // function. This only manifests in .NET Framework and is not an issue with Mono/.NET. + // The native return value is MarshaledVector without the wrapper because they are binary + // compatible. + [StructLayout(LayoutKind.Sequential)] + public struct CategoryNamesContainer + { + public MarshaledVector CategoryNames; + } + #pragma warning disable IDE0049 // Use built-in type alias #pragma warning disable SA1121 // Use built-in type alias @@ -64,7 +76,7 @@ private static class NativeMethods public delegate void DisposeGCHandleCallback(IntPtr handle); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - public delegate void LogMessageCallback(StringValue message, LogLevel level); + public delegate void LogMessageCallback(LogLevel level, StringValue categoryName, StringValue message); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate void HandleTaskCompletionCallback(IntPtr tcs_ptr, [MarshalAs(UnmanagedType.U1)] bool invoke_async, NativeException ex); @@ -223,8 +235,14 @@ public static extern void rename_property(SharedRealmHandle sharedRealm, [DllImport(InteropConfig.DLL_NAME, EntryPoint = "shared_realm_refresh_async", CallingConvention = CallingConvention.Cdecl)] public static extern bool refresh_async(SharedRealmHandle realm, IntPtr tcs_handle, out NativeException ex); + [DllImport(InteropConfig.DLL_NAME, EntryPoint = "shared_realm_get_log_level", CallingConvention = CallingConvention.Cdecl)] + public static extern LogLevel get_log_level([MarshalAs(UnmanagedType.LPWStr)] string category_name, IntPtr category_name_len); + [DllImport(InteropConfig.DLL_NAME, EntryPoint = "shared_realm_set_log_level", CallingConvention = CallingConvention.Cdecl)] - public static extern bool set_log_level(LogLevel level); + public static extern void set_log_level(LogLevel level, [MarshalAs(UnmanagedType.LPWStr)] string category_name, IntPtr category_name_len); + + [DllImport(InteropConfig.DLL_NAME, EntryPoint = "shared_realm_get_log_category_names", CallingConvention = CallingConvention.Cdecl)] + public static extern CategoryNamesContainer get_log_category_names(); [DllImport(InteropConfig.DLL_NAME, EntryPoint = "shared_realm_get_operating_system", CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr get_operating_system(IntPtr buffer, IntPtr buffer_length); @@ -271,7 +289,15 @@ public static unsafe void Initialize() notifyObject, notifyDictionary, onMigration, shouldCompact, handleTaskCompletion, onInitialization); } - public static void SetLogLevel(LogLevel level) => NativeMethods.set_log_level(level); + public static LogLevel GetLogLevel(LogCategory category) => NativeMethods.get_log_level(category.Name, (IntPtr)category.Name.Length); + + public static void SetLogLevel(LogLevel level, LogCategory category) => NativeMethods.set_log_level(level, category.Name, (IntPtr)category.Name.Length); + + public static string[] GetLogCategoryNames() => NativeMethods.get_log_category_names() + .CategoryNames + .ToEnumerable() + .Select(name => name.ToDotnetString()!) + .ToArray(); [Preserve] protected SharedRealmHandle(IntPtr handle) : base(handle) @@ -822,9 +848,9 @@ private static void OnDisposeGCHandle(IntPtr handle) } [MonoPInvokeCallback(typeof(NativeMethods.LogMessageCallback))] - private static void LogMessage(StringValue message, LogLevel level) + private static void LogMessage(LogLevel level, StringValue categoryName, StringValue message) { - Logger.LogDefault(level, message!); + RealmLogger.Default.LogAnyLevel(level, LogCategory.FromName(categoryName!), message!); } [MonoPInvokeCallback(typeof(NativeMethods.MigrationCallback))] diff --git a/Realm/Realm/Handles/SyncUserHandle.cs b/Realm/Realm/Handles/SyncUserHandle.cs index 11dcbb7cfd..fc385365d1 100644 --- a/Realm/Realm/Handles/SyncUserHandle.cs +++ b/Realm/Realm/Handles/SyncUserHandle.cs @@ -463,7 +463,7 @@ private static void HandleUserChanged(IntPtr managedUserHandle) } catch (Exception ex) { - Logger.Default.Log(LogLevel.Error, $"An error has occurred while raising User.Changed event: {ex}"); + RealmLogger.Default.Log(LogLevel.Error, $"An error has occurred while raising User.Changed event: {ex}"); } } } diff --git a/Realm/Realm/Helpers/Argument.cs b/Realm/Realm/Helpers/Argument.cs index 958bbf8903..bb674114b7 100644 --- a/Realm/Realm/Helpers/Argument.cs +++ b/Realm/Realm/Helpers/Argument.cs @@ -95,7 +95,7 @@ public static T ValidateNotNull(T value, string paramName) public static void AssertDebug(string message) { - Logger.LogDefault(LogLevel.Error, $"{message} {OpenIssueText}"); + RealmLogger.Default.Log(LogLevel.Error, $"{message} {OpenIssueText}"); #if DEBUG throw new Exception(message); diff --git a/Realm/Realm/Logging/LogCategory.cs b/Realm/Realm/Logging/LogCategory.cs new file mode 100644 index 0000000000..75095f31ba --- /dev/null +++ b/Realm/Realm/Logging/LogCategory.cs @@ -0,0 +1,227 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2024 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +using System.Collections.Generic; +using Realms.Helpers; + +namespace Realms.Logging +{ + /// + /// Specifies the category to receive log messages for when logged by the default + /// logger. The will always be set for a specific category. + /// Setting the log level for one category will automatically set the same level + /// for all of its subcategories. + ///

+ /// The category hierarchy is the following: + /// + /// Realm + /// ├─► Storage + /// │ ├─► Transaction + /// │ ├─► Query + /// │ ├─► Object + /// │ └─► Notification + /// ├─► Sync + /// │ ├─► Client + /// │ │ ├─► Session + /// │ │ ├─► Changeset + /// │ │ ├─► Network + /// │ │ └─► Reset + /// │ └─► Server + /// ├─► App + /// └─► SDK + /// + ///
+ /// + /// + /// LogCategory.Realm.Sync.Client + /// + /// + public class LogCategory + { + /// + /// Gets the name of the category. + /// + public string Name { get; } + + /// + /// Gets the top-level category for receiving log messages for all categories. + /// + public static RealmLogCategory Realm { get; } = new(); + + internal static readonly Dictionary NameToCategory = new() + { + { Realm.Name, Realm }, + { Realm.Storage.Name, Realm.Storage }, + { Realm.Storage.Transaction.Name, Realm.Storage.Transaction }, + { Realm.Storage.Query.Name, Realm.Storage.Query }, + { Realm.Storage.Object.Name, Realm.Storage.Object }, + { Realm.Storage.Notification.Name, Realm.Storage.Notification }, + { Realm.Sync.Name, Realm.Sync }, + { Realm.Sync.Client.Name, Realm.Sync.Client }, + { Realm.Sync.Client.Session.Name, Realm.Sync.Client.Session }, + { Realm.Sync.Client.Changeset.Name, Realm.Sync.Client.Changeset }, + { Realm.Sync.Client.Network.Name, Realm.Sync.Client.Network }, + { Realm.Sync.Client.Reset.Name, Realm.Sync.Client.Reset }, + { Realm.Sync.Server.Name, Realm.Sync.Server }, + { Realm.App.Name, Realm.App }, + { Realm.SDK.Name, Realm.SDK }, + }; + + private LogCategory(string name, LogCategory? parent) => Name = parent == null ? name : $"{parent}.{name}"; + + internal static LogCategory FromName(string name) + { + Argument.Ensure(NameToCategory.TryGetValue(name, out var category), $"Unexpected category name: '{name}'", nameof(name)); + + return category; + } + + /// + /// Returns a string that represents the category, equivalent to its name. + /// + /// A string that represents the category, equivalent to its name. + public override string ToString() => Name; + + /// + /// The top-level category for receiving log messages for all categories. + /// + public class RealmLogCategory : LogCategory + { + /// + /// Gets the category for receiving log messages pertaining to database events. + /// + public StorageLogCategory Storage { get; } + + /// + /// Gets the category for receiving log messages pertaining to Atlas Device Sync. + /// + public SyncLogCategory Sync { get; } + + /// + /// Gets the category for receiving log messages pertaining to Atlas App. + /// + public LogCategory App { get; } + + /// + /// Gets the category for receiving log messages pertaining to the SDK. + /// + public LogCategory SDK { get; } + + internal RealmLogCategory() : base("Realm", null) + { + Storage = new StorageLogCategory(this); + Sync = new SyncLogCategory(this); + App = new LogCategory("App", this); + SDK = new LogCategory("SDK", this); + } + } + + /// + /// The category for receiving log messages pertaining to database events. + /// + public class StorageLogCategory : LogCategory + { + /// + /// Gets the category for receiving log messages when creating, advancing, and + /// committing transactions. + /// + public LogCategory Transaction { get; } + + /// + /// Gets the category for receiving log messages when querying the database. + /// + public LogCategory Query { get; } + + /// + /// Gets the category for receiving log messages when mutating the database. + /// + public LogCategory Object { get; } + + /// + /// Gets the category for receiving log messages when there are notifications + /// of changes to the database. + /// + public LogCategory Notification { get; } + + internal StorageLogCategory(LogCategory parent) : base("Storage", parent) + { + Transaction = new LogCategory("Transaction", this); + Query = new LogCategory("Query", this); + Object = new LogCategory("Object", this); + Notification = new LogCategory("Notification", this); + } + } + + /// + /// The category for receiving log messages pertaining to Atlas Device Sync. + /// + public class SyncLogCategory : LogCategory + { + /// + /// Gets the category for receiving log messages pertaining to sync client operations. + /// + public ClientLogCategory Client { get; } + + /// + /// Gets the category for receiving log messages pertaining to sync server operations. + /// + public LogCategory Server { get; } + + internal SyncLogCategory(LogCategory parent) : base("Sync", parent) + { + Client = new ClientLogCategory(this); + Server = new LogCategory("Server", this); + } + } + + /// + /// The category for receiving log messages pertaining to sync client operations. + /// + public class ClientLogCategory : LogCategory + { + /// + /// Gets the category for receiving log messages pertaining to the sync session. + /// + public LogCategory Session { get; } + + /// + /// Gets the category for receiving log messages when receiving, uploading, and + /// integrating changesets. + /// + public LogCategory Changeset { get; } + + /// + /// Gets the category for receiving log messages pertaining to low-level network activity. + /// + public LogCategory Network { get; } + + /// + /// Gets the category for receiving log messages when there are client reset operations. + /// + public LogCategory Reset { get; } + + internal ClientLogCategory(LogCategory parent) : base("Client", parent) + { + Session = new LogCategory("Session", this); + Changeset = new LogCategory("Changeset", this); + Network = new LogCategory("Network", this); + Reset = new LogCategory("Reset", this); + } + } + } +} diff --git a/Realm/Realm/Logging/Logger.cs b/Realm/Realm/Logging/Logger.cs index a255969a39..da28f9a331 100644 --- a/Realm/Realm/Logging/Logger.cs +++ b/Realm/Realm/Logging/Logger.cs @@ -25,25 +25,43 @@ namespace Realms.Logging { + /// + [Obsolete("Use RealmLogger instead. If using a custom logger, RealmLogger.LogImpl() additionally receives the log category.")] + public abstract class Logger : RealmLogger + { + /// + /// The internal implementation being called from . + /// + /// The criticality level for the message. + /// The message to log. + protected abstract void LogImpl(LogLevel level, string message); + + /// + protected override void LogImpl(LogLevel level, LogCategory category, string message) + { + LogImpl(level, message); + } + } + /// /// A logger that logs messages originating from Realm. The default logger can be replaced by setting . ///
/// A few built-in implementations are provided by , , and , /// but you can implement your own. ///
- public abstract class Logger + public abstract class RealmLogger { private readonly Lazy _gcHandle; - private static Logger? _defaultLogger; - private static LogLevel _logLevel = LogLevel.Info; + private static readonly LogCategory _defaultLogCategory = LogCategory.Realm; + private static RealmLogger? _defaultLogger; /// /// Gets a that outputs messages to the default console. For most project types, that will be /// using but certain platforms may use different implementations. /// - /// A instance that outputs to the platform's console. - public static Logger Console { get; internal set; } = new ConsoleLogger(); + /// A instance that outputs to the platform's console. + public static RealmLogger Console { get; internal set; } = new ConsoleLogger(); /// /// Gets a that saves the log messages to a file. @@ -54,55 +72,89 @@ public abstract class Logger /// Please note that this logger is not optimized for performance, and could lead to overall sync performance slowdown with more verbose log levels. /// /// - /// A instance that will save log messages to a file. + /// A instance that will save log messages to a file. /// - public static Logger File(string filePath, Encoding? encoding = null) => new FileLogger(filePath, encoding); + public static RealmLogger File(string filePath, Encoding? encoding = null) => new FileLogger(filePath, encoding); /// /// Gets a that ignores all messages. /// - /// A that doesn't output any messages. - public static Logger Null { get; } = new NullLogger(); + /// A that doesn't output any messages. + public static RealmLogger Null { get; } = new NullLogger(); + + /// + /// Gets a that proxies Log calls to the supplied function. The message will + /// already be formatted with the default message formatting that includes a timestamp. + /// + /// Function to proxy log calls to. + /// + /// A instance that will invoke for each message. + /// + public static RealmLogger Function(Action logFunction) => new FunctionLogger((level, category, message) => logFunction(FormatLog(level, category, message))); /// /// Gets a that proxies Log calls to the supplied function. /// /// Function to proxy log calls to. /// - /// A instance that will invoke for each message. + /// A instance that will invoke for each message. /// - public static Logger Function(Action logFunction) => new FunctionLogger(logFunction); + [Obsolete("Use Function(Action logFunction).")] + public static RealmLogger Function(Action logFunction) => new FunctionLogger((level, _, message) => logFunction(level, message)); /// - /// Gets a that proxies Log calls to the supplied function. The message will - /// already be formatted with the default message formatting that includes a timestamp. + /// Gets a that proxies Log calls to the supplied function. /// /// Function to proxy log calls to. /// - /// A instance that will invoke for each message. + /// A instance that will invoke for each message. /// - public static Logger Function(Action logFunction) => new FunctionLogger((level, message) => logFunction(FormatLog(level, message))); + public static RealmLogger Function(Action logFunction) => new FunctionLogger(logFunction); /// - /// Gets or sets the verbosity of log messages. + /// Gets or sets the verbosity of log messages for all log categories via . /// /// The log level for Realm-originating messages. + [Obsolete("Use GetLogLevel() and SetLogLevel().")] public static LogLevel LogLevel { - get => _logLevel; + get => GetLogLevel(); set { - _logLevel = value; - SharedRealmHandle.SetLogLevel(value); + SetLogLevel(value); } } /// - /// Gets or sets a custom implementation that will be used by + /// Gets the verbosity of log messages for the given category. + /// + /// The category to get the level for. Defaults to if not specified. + /// + /// The log level used for the given category. + /// + public static LogLevel GetLogLevel(LogCategory? category = null) + { + category ??= _defaultLogCategory; + return SharedRealmHandle.GetLogLevel(category); + } + + /// + /// Sets the verbosity of log messages for the given category. + /// + /// The log level to use for messages. + /// The category to set the level for. Defaults to if not specified. + public static void SetLogLevel(LogLevel level, LogCategory? category = null) + { + category ??= _defaultLogCategory; + SharedRealmHandle.SetLogLevel(level, category); + } + + /// + /// Gets or sets a custom implementation that will be used by /// Realm whenever information must be logged. /// /// The logger to be used for Realm-originating messages. - public static Logger Default + public static RealmLogger Default { get => _defaultLogger ?? Console; set => _defaultLogger = value; @@ -111,57 +163,76 @@ public static Logger Default internal GCHandle GCHandle => _gcHandle.Value; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - protected Logger() + protected RealmLogger() { _gcHandle = new Lazy(() => GCHandle.Alloc(this)); } - internal static void LogDefault(LogLevel level, string message) => Default?.Log(level, message); - /// - /// Log a message at the supplied level. + /// Log a message at the supplied level and default category . /// /// The criticality level for the message. /// The message to log. public void Log(LogLevel level, string message) { - if (level < LogLevel) + Log(level, LogCategory.Realm.SDK, message); + } + + /// + /// Log a message at the supplied level and category. + /// + /// The criticality level for the message. + /// The category for the message. + /// The message to log. + public void Log(LogLevel level, LogCategory category, string message) + { + if (level < GetLogLevel(category)) { return; } + LogAnyLevel(level, category, message); + } + + /// + /// Log a message without calling into Core to check the current level. Logs from + /// Core should always call this API as they already check the level prior to notifying. + /// + internal void LogAnyLevel(LogLevel level, LogCategory category, string message) + { try { - LogImpl(level, message); + LogImpl(level, category, message); } catch (Exception ex) { - Console.Log(LogLevel.Error, $"An exception occurred while trying to log the message: '{message}' at level: {level}. Error: {ex}"); + Console.Log(LogLevel.Error, $"An exception occurred while trying to log the message: '{message}' at level: '{level}' in category: '{category}'. Error: {ex}"); } } /// - /// The internal implementation being called from . + /// The internal implementation being called from . /// /// The criticality level for the message. + /// The category for the message. /// The message to log. - protected abstract void LogImpl(LogLevel level, string message); + protected abstract void LogImpl(LogLevel level, LogCategory category, string message); - internal static string FormatLog(LogLevel level, string message) => $"{DateTimeOffset.UtcNow:yyyy-MM-dd HH:mm:ss.fff} {level}: {message}"; + internal static string FormatLog(LogLevel level, LogCategory category, string message) => $"{DateTimeOffset.UtcNow:yyyy-MM-dd HH:mm:ss.fff} {category} {level}: {message}"; - private class ConsoleLogger : Logger + private class ConsoleLogger : RealmLogger { - protected override void LogImpl(LogLevel level, string message) + protected override void LogImpl(LogLevel level, LogCategory category, string message) { - System.Console.WriteLine(FormatLog(level, message)); + System.Console.WriteLine(FormatLog(level, category, message)); } } - private class FileLogger : Logger + private class FileLogger : RealmLogger { - private readonly object locker = new(); + private readonly object _locker = new(); private readonly string _filePath; private readonly Encoding _encoding; @@ -171,43 +242,43 @@ public FileLogger(string filePath, Encoding? encoding = null) _encoding = encoding ?? Encoding.UTF8; } - protected override void LogImpl(LogLevel level, string message) + protected override void LogImpl(LogLevel level, LogCategory category, string message) { - lock (locker) + lock (_locker) { - System.IO.File.AppendAllText(_filePath, FormatLog(level, message) + Environment.NewLine, _encoding); + System.IO.File.AppendAllText(_filePath, FormatLog(level, category, message) + Environment.NewLine, _encoding); } } } - private class FunctionLogger : Logger + private class FunctionLogger : RealmLogger { - private readonly Action _logFunction; + private readonly Action _logFunction; - public FunctionLogger(Action logFunction) + public FunctionLogger(Action logFunction) { _logFunction = logFunction; } - protected override void LogImpl(LogLevel level, string message) => _logFunction(level, message); + protected override void LogImpl(LogLevel level, LogCategory category, string message) => _logFunction(level, category, message); } - private class NullLogger : Logger + private class NullLogger : RealmLogger { - protected override void LogImpl(LogLevel level, string message) + protected override void LogImpl(LogLevel level, LogCategory category, string message) { } } - internal class InMemoryLogger : Logger + internal class InMemoryLogger : RealmLogger { private readonly StringBuilder _builder = new(); - protected override void LogImpl(LogLevel level, string message) + protected override void LogImpl(LogLevel level, LogCategory category, string message) { lock (_builder) { - _builder.AppendLine(FormatLog(level, message)); + _builder.AppendLine(FormatLog(level, category, message)); } } @@ -228,7 +299,7 @@ public void Clear() } } - internal class AsyncFileLogger : Logger, IDisposable + internal class AsyncFileLogger : RealmLogger, IDisposable { private readonly ConcurrentQueue _queue = new(); private readonly string _filePath; @@ -255,11 +326,11 @@ public void Dispose() _flush.Dispose(); } - protected override void LogImpl(LogLevel level, string message) + protected override void LogImpl(LogLevel level, LogCategory category, string message) { if (!_isFlushing) { - _queue.Enqueue(FormatLog(level, message)); + _queue.Enqueue(FormatLog(level, category, message)); _hasNewItems.Set(); } } diff --git a/Realm/Realm/Native/NativeCommon.cs b/Realm/Realm/Native/NativeCommon.cs index 45e718b9e1..9dc192fc9e 100644 --- a/Realm/Realm/Native/NativeCommon.cs +++ b/Realm/Realm/Native/NativeCommon.cs @@ -96,7 +96,7 @@ public static void CleanupNativeResources(string reason) { if (Interlocked.CompareExchange(ref _isInitialized, 0, 1) == 1) { - Logger.LogDefault(LogLevel.Info, $"Realm: Force closing all native instances: {reason}"); + RealmLogger.Default.Log(LogLevel.Info, $"Realm: Force closing all native instances: {reason}"); var sw = new Stopwatch(); sw.Start(); @@ -106,12 +106,12 @@ public static void CleanupNativeResources(string reason) SharedRealmHandle.ForceCloseNativeRealms(); sw.Stop(); - Logger.LogDefault(LogLevel.Info, $"Realm: Closed all native instances in {sw.ElapsedMilliseconds} ms."); + RealmLogger.Default.Log(LogLevel.Info, $"Realm: Closed all native instances in {sw.ElapsedMilliseconds} ms."); } } catch (Exception ex) { - Logger.LogDefault(LogLevel.Error, $"Realm: Failed to close all native instances. You may need to restart your app. Error: {ex}"); + RealmLogger.Default.Log(LogLevel.Error, $"Realm: Failed to close all native instances. You may need to restart your app. Error: {ex}"); } } diff --git a/Realm/Realm/Native/SyncSocketProvider.EventLoop.cs b/Realm/Realm/Native/SyncSocketProvider.EventLoop.cs index 369da7daf7..d74a5f5161 100644 --- a/Realm/Realm/Native/SyncSocketProvider.EventLoop.cs +++ b/Realm/Realm/Native/SyncSocketProvider.EventLoop.cs @@ -32,7 +32,7 @@ private class Timer internal Timer(TimeSpan delay, IntPtr nativeCallback, ChannelWriter workQueue) { - Logger.LogDefault(LogLevel.Trace, $"Creating timer with delay {delay} and target {nativeCallback}."); + RealmLogger.Default.Log(LogLevel.Trace, $"Creating timer with delay {delay} and target {nativeCallback}."); var cancellationToken = _cts.Token; Task.Delay(delay, cancellationToken).ContinueWith(async _ => { @@ -42,7 +42,7 @@ internal Timer(TimeSpan delay, IntPtr nativeCallback, ChannelWriter workQ internal void Cancel() { - Logger.LogDefault(LogLevel.Trace, $"Canceling timer."); + RealmLogger.Default.Log(LogLevel.Trace, $"Canceling timer."); _cts.Cancel(); _cts.Dispose(); } @@ -72,7 +72,7 @@ public void Execute() { if (cancellationToken.IsCancellationRequested) { - Logger.LogDefault(LogLevel.Trace, "Deleting EventLoopWork callback only because event loop was cancelled."); + RealmLogger.Default.Log(LogLevel.Trace, "Deleting EventLoopWork callback only because event loop was cancelled."); NativeMethods.delete_callback(nativeCallback); return; } @@ -83,7 +83,7 @@ public void Execute() private static void RunCallback(IntPtr nativeCallback, Status status) { - Logger.LogDefault(LogLevel.Trace, $"SyncSocketProvider running native callback {nativeCallback} with status {status.Code} \"{status.Reason}\"."); + RealmLogger.Default.Log(LogLevel.Trace, $"SyncSocketProvider running native callback {nativeCallback} with status {status.Code} \"{status.Reason}\"."); using var arena = new Arena(); NativeMethods.run_callback(nativeCallback, status.Code, StringValue.AllocateFrom(status.Reason, arena)); @@ -91,13 +91,13 @@ private static void RunCallback(IntPtr nativeCallback, Status status) private async Task PostWorkAsync(IntPtr nativeCallback) { - Logger.LogDefault(LogLevel.Trace, "Posting work to SyncSocketProvider event loop."); + RealmLogger.Default.Log(LogLevel.Trace, "Posting work to SyncSocketProvider event loop."); await _workQueue.Writer.WriteAsync(new EventLoopWork(nativeCallback, _cts.Token)); } private async partial Task WorkThread() { - Logger.LogDefault(LogLevel.Trace, "Starting SyncSocketProvider event loop."); + RealmLogger.Default.Log(LogLevel.Trace, "Starting SyncSocketProvider event loop."); try { while (await _workQueue.Reader.WaitToReadAsync()) @@ -110,15 +110,15 @@ private async partial Task WorkThread() } catch (Exception e) { - Logger.LogDefault(LogLevel.Error, $"Error occurred in SyncSocketProvider event loop {e.GetType().FullName}: {e.Message}"); + RealmLogger.Default.Log(LogLevel.Error, $"Error occurred in SyncSocketProvider event loop {e.GetType().FullName}: {e.Message}"); if (!string.IsNullOrEmpty(e.StackTrace)) { - Logger.LogDefault(LogLevel.Trace, e.StackTrace); + RealmLogger.Default.Log(LogLevel.Trace, e.StackTrace); } throw; } - Logger.LogDefault(LogLevel.Trace, "Exiting SyncSocketProvider event loop."); + RealmLogger.Default.Log(LogLevel.Trace, "Exiting SyncSocketProvider event loop."); } } diff --git a/Realm/Realm/Native/SyncSocketProvider.WebSocket.cs b/Realm/Realm/Native/SyncSocketProvider.WebSocket.cs index 4990a356e5..5aa91cc811 100644 --- a/Realm/Realm/Native/SyncSocketProvider.WebSocket.cs +++ b/Realm/Realm/Native/SyncSocketProvider.WebSocket.cs @@ -45,7 +45,7 @@ private class Socket : IDisposable internal Socket(ClientWebSocket webSocket, IntPtr observer, ChannelWriter workQueue, Uri uri) { - Logger.LogDefault(LogLevel.Trace, $"Creating a WebSocket to {uri.GetLeftPart(UriPartial.Path)}"); + RealmLogger.Default.Log(LogLevel.Trace, $"Creating a WebSocket to {uri.GetLeftPart(UriPartial.Path)}"); _webSocket = webSocket; _observer = observer; _workQueue = workQueue; @@ -56,7 +56,7 @@ internal Socket(ClientWebSocket webSocket, IntPtr observer, ChannelWriter private async Task ReadThread() { - Logger.LogDefault(LogLevel.Trace, "Entering WebSocket event loop."); + RealmLogger.Default.Log(LogLevel.Trace, "Entering WebSocket event loop."); try { @@ -67,7 +67,7 @@ private async Task ReadThread() { var builder = new StringBuilder(); FormatExceptionForLogging(e, builder); - Logger.LogDefault(LogLevel.Error, $"Error establishing WebSocket connection {builder}"); + RealmLogger.Default.Log(LogLevel.Error, $"Error establishing WebSocket connection {builder}"); await _workQueue.WriteAsync(new WebSocketClosedWork(false, (WebSocketCloseStatus)RLM_ERR_WEBSOCKET_CONNECTION_FAILED, e.Message, _observer, _cancellationToken)); return; @@ -92,11 +92,11 @@ private async Task ReadThread() break; case WebSocketMessageType.Close: - Logger.LogDefault(LogLevel.Trace, $"WebSocket closed with status {result.CloseStatus!.Value} and description \"{result.CloseStatusDescription}\""); + RealmLogger.Default.Log(LogLevel.Trace, $"WebSocket closed with status {result.CloseStatus!.Value} and description \"{result.CloseStatusDescription}\""); await _workQueue.WriteAsync(new WebSocketClosedWork(clean: true, result.CloseStatus!.Value, result.CloseStatusDescription!, _observer, _cancellationToken)); break; default: - Logger.LogDefault(LogLevel.Trace, $"Received unexpected text WebSocket message: {Encoding.UTF8.GetString(buffer, 0, result.Count)}"); + RealmLogger.Default.Log(LogLevel.Trace, $"Received unexpected text WebSocket message: {Encoding.UTF8.GetString(buffer, 0, result.Count)}"); break; } } @@ -104,7 +104,7 @@ private async Task ReadThread() { var builder = new StringBuilder(); FormatExceptionForLogging(e, builder); - Logger.LogDefault(LogLevel.Error, $"Error reading from WebSocket {builder}"); + RealmLogger.Default.Log(LogLevel.Error, $"Error reading from WebSocket {builder}"); await _workQueue.WriteAsync(new WebSocketClosedWork(false, (WebSocketCloseStatus)RLM_ERR_WEBSOCKET_READ_ERROR, e.Message, _observer, _cancellationToken)); return; @@ -130,7 +130,7 @@ public async void Write(BinaryValue data, IntPtr native_callback) { var builder = new StringBuilder(); FormatExceptionForLogging(e, builder); - Logger.LogDefault(LogLevel.Error, $"Error writing to WebSocket {builder}"); + RealmLogger.Default.Log(LogLevel.Error, $"Error writing to WebSocket {builder}"); // in case of errors notify the websocket observer and just dispose the callback await _workQueue.WriteAsync(new WebSocketClosedWork(false, (WebSocketCloseStatus)RLM_ERR_WEBSOCKET_WRITE_ERROR, e.Message, _observer, _cancellationToken)); @@ -164,7 +164,7 @@ public async void Dispose() _webSocket.Dispose(); _receiveBuffer.Dispose(); _cts.Dispose(); - Logger.LogDefault(LogLevel.Trace, "Disposing WebSocket."); + RealmLogger.Default.Log(LogLevel.Trace, "Disposing WebSocket."); try { @@ -185,7 +185,7 @@ private static void FormatExceptionForLogging(Exception ex, StringBuilder builde builder.AppendFormat("{0}: {1}", ex.GetType().FullName, ex.Message); builder.AppendLine(); - if (Logger.LogLevel >= LogLevel.Trace && !string.IsNullOrEmpty(ex.StackTrace)) + if (RealmLogger.GetLogLevel(LogCategory.Realm.SDK) >= LogLevel.Trace && !string.IsNullOrEmpty(ex.StackTrace)) { builder.Append(indentation); var indentedTrace = ex.StackTrace.Replace(Environment.NewLine, Environment.NewLine + indentation); diff --git a/Realm/Realm/Native/SyncSocketProvider.cs b/Realm/Realm/Native/SyncSocketProvider.cs index 2cc5e67686..e281847a9f 100644 --- a/Realm/Realm/Native/SyncSocketProvider.cs +++ b/Realm/Realm/Native/SyncSocketProvider.cs @@ -156,7 +156,7 @@ private interface IWork internal SyncSocketProvider(Action? onWebSocketConnection) { - Logger.LogDefault(LogLevel.Debug, "Creating SyncSocketProvider."); + RealmLogger.Default.Log(LogLevel.Debug, "Creating SyncSocketProvider."); _onWebSocketConnection = onWebSocketConnection; _workQueue = Channel.CreateUnbounded(new() { SingleReader = true }); _workThread = Task.Run(WorkThread); @@ -166,7 +166,7 @@ internal SyncSocketProvider(Action? onWebSocketConnectio public void Dispose() { - Logger.LogDefault(LogLevel.Debug, "Destroying SyncSocketProvider."); + RealmLogger.Default.Log(LogLevel.Debug, "Destroying SyncSocketProvider."); _workQueue.Writer.Complete(); _cts.Cancel(); _cts.Dispose(); diff --git a/Realm/Realm/Realm.cs b/Realm/Realm/Realm.cs index 9d741994d7..1aa7bdb83a 100644 --- a/Realm/Realm/Realm.cs +++ b/Realm/Realm/Realm.cs @@ -414,7 +414,7 @@ internal void NotifyError(Exception ex) { if (Error is null) { - Logger.LogDefault(LogLevel.Error, "A realm-level exception has occurred. To handle and react to those, subscribe to the Realm.Error event."); + RealmLogger.Default.Log(LogLevel.Error, "A realm-level exception has occurred. To handle and react to those, subscribe to the Realm.Error event."); } Error?.Invoke(this, new ErrorEventArgs(ex)); diff --git a/Realm/Realm/Sync/ProgressNotifications/ProgressNotificationToken.cs b/Realm/Realm/Sync/ProgressNotifications/ProgressNotificationToken.cs index 57674fc30a..03a74eb692 100644 --- a/Realm/Realm/Sync/ProgressNotifications/ProgressNotificationToken.cs +++ b/Realm/Realm/Sync/ProgressNotifications/ProgressNotificationToken.cs @@ -58,7 +58,7 @@ public void Notify(double progressEstimate) } catch (Exception ex) { - Logger.Default.Log(LogLevel.Warn, $"An error occurred while reporting progress: {ex}"); + RealmLogger.Default.Log(LogLevel.Warn, $"An error occurred while reporting progress: {ex}"); } }); } diff --git a/Tests/Realm.Tests/Database/GuidRepresentationMigrationTests.cs b/Tests/Realm.Tests/Database/GuidRepresentationMigrationTests.cs index 0984eda9ad..187af873d7 100644 --- a/Tests/Realm.Tests/Database/GuidRepresentationMigrationTests.cs +++ b/Tests/Realm.Tests/Database/GuidRepresentationMigrationTests.cs @@ -55,8 +55,8 @@ public void Migration_FlipGuid_ShouldProduceCorrectRepresentation() [Test] public void Migration_FromLittleEndianGuidFile([Values(true, false)] bool useLegacyRepresentation) { - var logger = new Logger.InMemoryLogger(); - Logger.Default = logger; + var logger = new RealmLogger.InMemoryLogger(); + RealmLogger.Default = logger; #pragma warning disable CS0618 // Type or member is obsolete Realm.UseLegacyGuidRepresentation = useLegacyRepresentation; @@ -94,8 +94,8 @@ public void Migration_FromLittleEndianGuidFile([Values(true, false)] bool useLeg [Test] public void PopulatingANewFile([Values(true, false)] bool useLegacyRepresentation) { - var logger = new Logger.InMemoryLogger(); - Logger.Default = logger; + var logger = new RealmLogger.InMemoryLogger(); + RealmLogger.Default = logger; #pragma warning disable CS0618 // Type or member is obsolete Realm.UseLegacyGuidRepresentation = useLegacyRepresentation; @@ -171,8 +171,8 @@ public void FlexibleSync_Subscriptions_MatchesGuid([Values(true, false)] bool us [Test] public void UnmigratedRealm_WhenOpenedAsReadonly_LogsAMessageAndDoesntChangeFile() { - var logger = new Logger.InMemoryLogger(); - Logger.Default = logger; + var logger = new RealmLogger.InMemoryLogger(); + RealmLogger.Default = logger; TestHelpers.CopyBundledFileToDocuments("guids.realm", _configuration.DatabasePath); _configuration.IsReadOnly = true; @@ -213,8 +213,8 @@ public void MigratedRealm_WhenOpenedAsReadonly_DoesntDoAnything() // Open the Realm to migrate it } - var logger = new Logger.InMemoryLogger(); - Logger.Default = logger; + var logger = new RealmLogger.InMemoryLogger(); + RealmLogger.Default = logger; _configuration.IsReadOnly = true; @@ -243,8 +243,8 @@ public void Migration_FromLittleEndian_WhenContainingAmbiguousGuids_LogsWarning( { // This tests that a file that doesn't appear to have little-endian guids is not migrated // See comment in guid_representation_migration.cpp/flip_guid - var logger = new Logger.InMemoryLogger(); - Logger.Default = logger; + var logger = new RealmLogger.InMemoryLogger(); + RealmLogger.Default = logger; TestHelpers.CopyBundledFileToDocuments("bad-guids.realm", _configuration.DatabasePath); @@ -273,8 +273,8 @@ public void Migration_FromLittleEndian_WhenContainingBothGoodAndBadGuids_LogsWar // This tests that a file that contains both ambiguous (xxxxxxxx-xxxx-4x4x-xxxx-xxxxxxxx) and unambiguous guids // does get migrated. // See comment in guid_representation_migration.cpp/flip_guid - var logger = new Logger.InMemoryLogger(); - Logger.Default = logger; + var logger = new RealmLogger.InMemoryLogger(); + RealmLogger.Default = logger; TestHelpers.CopyBundledFileToDocuments("mixed-guids.realm", _configuration.DatabasePath); @@ -301,8 +301,8 @@ public void Migration_FromLittleEndian_WhenContainingBothGoodAndBadGuids_LogsWar [Test] public void SynchronizedRealm_DoesntMigrate([Values(true, false)] bool useLegacyRepresentation) { - var logger = new Logger.InMemoryLogger(); - Logger.Default = logger; + var logger = new RealmLogger.InMemoryLogger(); + RealmLogger.Default = logger; #pragma warning disable CS0618 // Type or member is obsolete Realm.UseLegacyGuidRepresentation = useLegacyRepresentation; diff --git a/Tests/Realm.Tests/Database/InstanceTests.cs b/Tests/Realm.Tests/Database/InstanceTests.cs index e920e1c9e7..0383366379 100644 --- a/Tests/Realm.Tests/Database/InstanceTests.cs +++ b/Tests/Realm.Tests/Database/InstanceTests.cs @@ -1271,8 +1271,8 @@ public void RealmDispose_DisposesActiveTransaction() [Test] public void Logger_ChangeLevel_ReflectedImmediately() { - var logger = new Logger.InMemoryLogger(); - Logger.Default = logger; + var logger = new RealmLogger.InMemoryLogger(); + RealmLogger.Default = logger; using var realm = GetRealm(Guid.NewGuid().ToString()); @@ -1281,14 +1281,14 @@ public void Logger_ChangeLevel_ReflectedImmediately() // We're at info level, so we don't expect any statements. WriteAndVerifyLogs(); - Logger.LogLevel = LogLevel.Debug; + RealmLogger.SetLogLevel(LogLevel.Debug); // We're at Debug level now, so we should see the write message. var expectedWriteLog = new Regex("Debug: DB: .* Commit of size [^ ]* done in [^ ]* us"); WriteAndVerifyLogs(expectedWriteLog); // Revert back to Info level and make sure we don't log anything - Logger.LogLevel = LogLevel.Info; + RealmLogger.SetLogLevel(LogLevel.Info); WriteAndVerifyLogs(); void WriteAndVerifyLogs(Regex? expectedRegex = null) diff --git a/Tests/Realm.Tests/Database/LoggerTests.cs b/Tests/Realm.Tests/Database/LoggerTests.cs index 3c122dc907..522b0f3b72 100644 --- a/Tests/Realm.Tests/Database/LoggerTests.cs +++ b/Tests/Realm.Tests/Database/LoggerTests.cs @@ -27,68 +27,247 @@ namespace Realms.Tests.Database [TestFixture, Preserve(AllMembers = true)] public class LoggerTests { - private Logger _originalLogger = null!; - private LogLevel _originalLogLevel; + private readonly LogCategory _originalLogCategory = LogCategory.Realm; + private readonly LogLevel _originalLogLevel = RealmLogger.GetLogLevel(LogCategory.Realm); + private RealmLogger _originalLogger = null!; + + private class UserDefinedLogger : RealmLogger + { + private readonly Action _logFunction; + + public UserDefinedLogger(Action logFunction) + { + _logFunction = logFunction; + } + + protected override void LogImpl(LogLevel level, LogCategory category, string message) => _logFunction(level, category, message); + } + + [Obsolete("Using obsolete logger.")] + private class ObsoleteUserDefinedLogger : Logger + { + private readonly Action _logFunction; + + public ObsoleteUserDefinedLogger(Action logFunction) + { + _logFunction = logFunction; + } + + protected override void LogImpl(LogLevel level, string message) => _logFunction(level, message); + } [SetUp] public void Setup() { - _originalLogger = Logger.Default; - _originalLogLevel = Logger.LogLevel; + _originalLogger = RealmLogger.Default; } [TearDown] public void TearDown() { - Logger.Default = _originalLogger; - Logger.LogLevel = _originalLogLevel; + RealmLogger.Default = _originalLogger; + RealmLogger.SetLogLevel(_originalLogLevel, _originalLogCategory); + } + + private void AssertLogMessageContains(string actual, LogLevel level, LogCategory category, string message) + { + Assert.That(actual, Does.Contain(level.ToString())); + Assert.That(actual, Does.Contain(category.Name)); + Assert.That(actual, Does.Contain(message)); + Assert.That(actual, Does.Contain(DateTimeOffset.UtcNow.ToString("yyyy-MM-dd"))); + } + + [Test] + public void Logger_CanSetDefaultLoggerToBuiltInLogger() + { + var messages = new List(); + RealmLogger.Default = RealmLogger.Function(message => messages.Add(message)); + + RealmLogger.Default.Log(LogLevel.Warn, LogCategory.Realm.SDK, "This is very dangerous!"); + + Assert.That(messages.Count, Is.EqualTo(1)); + AssertLogMessageContains(messages[0], LogLevel.Warn, LogCategory.Realm.SDK, "This is very dangerous!"); + } + + [Test] + public void Logger_CanSetDefaultLoggerToUserDefinedLogger() + { + var messages = new List(); + RealmLogger.Default = new UserDefinedLogger((level, category, message) => messages.Add(RealmLogger.FormatLog(level, category, message))); + + RealmLogger.Default.Log(LogLevel.Warn, LogCategory.Realm.SDK, "A log message"); + + Assert.That(messages.Count, Is.EqualTo(1)); + AssertLogMessageContains(messages[0], LogLevel.Warn, LogCategory.Realm.SDK, "A log message"); } [Test] - public void Logger_CanSetDefaultLogger() + [Obsolete("Using obsolete logger class.")] + public void ObsoleteLogger_CanSetDefaultLoggerToUserDefinedLogger() { var messages = new List(); - Logger.Default = Logger.Function(message => messages.Add(message)); + Logger.Default = new ObsoleteUserDefinedLogger((level, message) => messages.Add(Logger.FormatLog(level, LogCategory.Realm.SDK, message))); - Logger.LogDefault(LogLevel.Warn, "This is very dangerous!"); + Logger.Default.Log(LogLevel.Warn, "A log message"); Assert.That(messages.Count, Is.EqualTo(1)); - Assert.That(messages[0], Does.Contain(LogLevel.Warn.ToString())); - Assert.That(messages[0], Does.Contain(DateTimeOffset.UtcNow.ToString("yyyy-MM-dd"))); - Assert.That(messages[0], Does.Contain("This is very dangerous!")); + AssertLogMessageContains(messages[0], LogLevel.Warn, LogCategory.Realm.SDK, "A log message"); } [Test] public void Logger_SkipsDebugMessagesByDefault() { var messages = new List(); - Logger.Default = Logger.Function(message => messages.Add(message)); + RealmLogger.Default = RealmLogger.Function(message => messages.Add(message)); - Logger.LogDefault(LogLevel.Debug, "This is a debug message!"); + RealmLogger.Default.Log(LogLevel.Debug, "This is a debug message!"); Assert.That(messages.Count, Is.EqualTo(0)); } + [Test] + public void Logger_SetsLogLevelAtGivenCategory() + { + var categories = LogCategory.NameToCategory.Values; + foreach (var category in categories) + { + RealmLogger.SetLogLevel(LogLevel.All, category); + Assert.That(RealmLogger.GetLogLevel(category), Is.EqualTo(LogLevel.All)); + } + } + + [Test] + public void Logger_SetsLogLevelAtSubcategories() + { + var storageCategories = new[] + { + LogCategory.Realm.Storage.Transaction, + LogCategory.Realm.Storage.Query, + LogCategory.Realm.Storage.Object, + LogCategory.Realm.Storage.Notification + }; + foreach (var category in storageCategories) + { + Assert.That(RealmLogger.GetLogLevel(category), Is.Not.EqualTo(LogLevel.Error)); + } + + RealmLogger.SetLogLevel(LogLevel.Error, LogCategory.Realm.Storage); + foreach (var category in storageCategories) + { + Assert.That(RealmLogger.GetLogLevel(category), Is.EqualTo(LogLevel.Error)); + } + } + + [Test] + [Obsolete("Using LogLevel set accessor.")] + public void Logger_WhenUsingLogLevelSetter_OverwritesCategory() + { + var category = LogCategory.Realm.Storage; + RealmLogger.SetLogLevel(LogLevel.Error, category); + Assert.That(RealmLogger.GetLogLevel(category), Is.EqualTo(LogLevel.Error)); + + RealmLogger.LogLevel = LogLevel.All; + Assert.That(RealmLogger.GetLogLevel(category), Is.EqualTo(LogLevel.All)); + } + [TestCase(LogLevel.Error)] [TestCase(LogLevel.Info)] [TestCase(LogLevel.Debug)] public void Logger_WhenLevelIsSet_LogsOnlyExpectedLevels(LogLevel level) + { + var categories = LogCategory.NameToCategory.Values; + foreach (var category in categories) + { + var messages = new List(); + RealmLogger.Default = RealmLogger.Function(message => messages.Add(message)); + RealmLogger.SetLogLevel(level, category); + + RealmLogger.Default.Log(level - 1, category, "This is at level - 1"); + RealmLogger.Default.Log(level, category, "This is at the same level"); + RealmLogger.Default.Log(level + 1, category, "This is at level + 1"); + + Assert.That(messages.Count, Is.EqualTo(2)); + AssertLogMessageContains(messages[0], level, category, "This is at the same level"); + AssertLogMessageContains(messages[1], level + 1, category, "This is at level + 1"); + } + } + + [Test] + public void Logger_LogsAtGivenCategory() + { + var categories = LogCategory.NameToCategory.Values; + foreach (var category in categories) + { + var messages = new List(); + RealmLogger.Default = RealmLogger.Function((message) => messages.Add(message)); + + RealmLogger.Default.Log(LogLevel.Warn, category, "A log message"); + + Assert.That(messages.Count, Is.EqualTo(1)); + AssertLogMessageContains(messages[0], LogLevel.Warn, category, "A log message"); + } + } + + [Test] + public void Logger_LogsSdkCategoryByDefault() { var messages = new List(); - Logger.Default = Logger.Function(message => messages.Add(message)); - Logger.LogLevel = level; + RealmLogger.Default = RealmLogger.Function((message) => messages.Add(message)); - Logger.LogDefault(level - 1, "This is at level - 1"); - Logger.LogDefault(level, "This is at the same level"); - Logger.LogDefault(level + 1, "This is at level + 1"); + RealmLogger.Default.Log(LogLevel.Warn, "A log message"); - Assert.That(messages.Count, Is.EqualTo(2)); + Assert.That(messages.Count, Is.EqualTo(1)); + AssertLogMessageContains(messages[0], LogLevel.Warn, LogCategory.Realm.SDK, "A log message"); + } - Assert.That(messages[0], Does.Contain(level.ToString())); - Assert.That(messages[0], Does.Contain("This is at the same level")); + [Test] + public void Logger_CallsCustomFunction() + { + var messages = new List(); + RealmLogger.Default = RealmLogger.Function((level, category, message) => messages.Add(RealmLogger.FormatLog(level, category, message))); - Assert.That(messages[1], Does.Contain((level + 1).ToString())); - Assert.That(messages[1], Does.Contain("This is at level + 1")); + RealmLogger.Default.Log(LogLevel.Warn, LogCategory.Realm.SDK, "A log message"); + + Assert.That(messages.Count, Is.EqualTo(1)); + AssertLogMessageContains(messages[0], LogLevel.Warn, LogCategory.Realm.SDK, "A log message"); + } + + [Test] + [Obsolete("Using function not accepting category.")] + public void Logger_CallsObsoleteCustomFunction() + { + var messages = new List(); + RealmLogger.Default = RealmLogger.Function((level, message) => messages.Add(RealmLogger.FormatLog(level, LogCategory.Realm.SDK, message))); + + RealmLogger.Default.Log(LogLevel.Warn, "A log message"); + + Assert.That(messages.Count, Is.EqualTo(1)); + AssertLogMessageContains(messages[0], LogLevel.Warn, LogCategory.Realm.SDK, "A log message"); + } + + [Test] + public void Logger_MatchesCoreCategoryNames() + { + var coreCategoryNames = SharedRealmHandle.GetLogCategoryNames(); + var sdkCategoriesMap = LogCategory.NameToCategory; + + Assert.That(sdkCategoriesMap.Count, Is.EqualTo(coreCategoryNames.Length)); + foreach (var name in coreCategoryNames) + { + Assert.That(sdkCategoriesMap.TryGetValue(name!, out var category), Is.True); + Assert.That(category!.Name, Is.EqualTo(name)); + Assert.That(LogCategory.FromName(name!), Is.SameAs(category)); + } + } + + [Test] + public void Logger_WhenNonExistentCategoryName_FromNameThrows() + { + var nonExistentNames = new[] { "realm", "Realm.app", string.Empty }; + foreach (var name in nonExistentNames) + { + Assert.That(() => LogCategory.FromName(name), Throws.TypeOf().And.Message.Contains($"Unexpected category name: '{name}'")); + } } [Test] @@ -96,17 +275,17 @@ public void FileLogger() { var tempFilePath = Path.GetTempFileName(); - Logger.LogLevel = LogLevel.All; - Logger.Default = Logger.File(tempFilePath); + RealmLogger.SetLogLevel(LogLevel.All); + RealmLogger.Default = RealmLogger.File(tempFilePath); var warnMessage = "This is very dangerous!"; var debugMessage = "This is a debug message!"; var errorMessage = "This is an error!"; var timeString = DateTimeOffset.UtcNow.ToString("yyyy-MM-dd"); - Logger.LogDefault(LogLevel.Warn, warnMessage); - Logger.LogDefault(LogLevel.Debug, debugMessage); - Logger.LogDefault(LogLevel.Error, errorMessage); + RealmLogger.Default.Log(LogLevel.Warn, warnMessage); + RealmLogger.Default.Log(LogLevel.Debug, debugMessage); + RealmLogger.Default.Log(LogLevel.Error, errorMessage); var loggedStrings = File.ReadAllLines(tempFilePath); diff --git a/Tests/Realm.Tests/Database/NotificationTests.cs b/Tests/Realm.Tests/Database/NotificationTests.cs index 96e99f754e..91d9796220 100644 --- a/Tests/Realm.Tests/Database/NotificationTests.cs +++ b/Tests/Realm.Tests/Database/NotificationTests.cs @@ -53,8 +53,8 @@ public void ShouldTriggerRealmChangedEvent() [Test] public void RealmError_WhenNoSubscribers_OutputsMessageInConsole() { - var logger = new Logger.InMemoryLogger(); - Logger.Default = logger; + var logger = new RealmLogger.InMemoryLogger(); + RealmLogger.Default = logger; _realm.NotifyError(new Exception()); Assert.That(logger.GetLog(), Does.Contain("exception").And.Contains("Realm.Error")); diff --git a/Tests/Realm.Tests/RealmTest.cs b/Tests/Realm.Tests/RealmTest.cs index aa5955c48b..593eaec391 100644 --- a/Tests/Realm.Tests/RealmTest.cs +++ b/Tests/Realm.Tests/RealmTest.cs @@ -32,8 +32,9 @@ namespace Realms.Tests public abstract class RealmTest { private readonly ConcurrentQueue> _realms = new(); - private Logger _originalLogger = null!; - private LogLevel _originalLogLevel; + private readonly LogCategory _originalLogCategory = LogCategory.Realm; + private readonly LogLevel _originalLogLevel = RealmLogger.GetLogLevel(LogCategory.Realm); + private RealmLogger _originalLogger = null!; private bool _isSetup; @@ -57,8 +58,7 @@ public void SetUp() { if (!_isSetup) { - _originalLogger = Logger.Default; - _originalLogLevel = Logger.LogLevel; + _originalLogger = RealmLogger.Default; if (OverrideDefaultConfig) { @@ -86,8 +86,8 @@ public void TearDown() { CustomTearDown(); - Logger.Default = _originalLogger; - Logger.LogLevel = _originalLogLevel; + RealmLogger.Default = _originalLogger; + RealmLogger.SetLogLevel(_originalLogLevel, _originalLogCategory); #pragma warning disable CS0618 // Type or member is obsolete Realm.UseLegacyGuidRepresentation = false; diff --git a/Tests/Realm.Tests/Sync/AppTests.cs b/Tests/Realm.Tests/Sync/AppTests.cs index 4b89bd8926..5a538b4d8e 100644 --- a/Tests/Realm.Tests/Sync/AppTests.cs +++ b/Tests/Realm.Tests/Sync/AppTests.cs @@ -167,9 +167,9 @@ public void RealmConfiguration_WithCustomLogger_LogsSyncOperations(LogLevel logL { SyncTestHelpers.RunBaasTestAsync(async () => { - Logger.LogLevel = logLevel; - var logger = new Logger.InMemoryLogger(); - Logger.Default = logger; + RealmLogger.SetLogLevel(logLevel); + var logger = new RealmLogger.InMemoryLogger(); + RealmLogger.Default = logger; var config = await GetIntegrationConfigAsync(Guid.NewGuid().ToString()); using var realm = await GetRealmAsync(config); diff --git a/Tests/Realm.Tests/Sync/SyncTestHelpers.cs b/Tests/Realm.Tests/Sync/SyncTestHelpers.cs index 3e9dcdfec1..40a0546ca8 100644 --- a/Tests/Realm.Tests/Sync/SyncTestHelpers.cs +++ b/Tests/Realm.Tests/Sync/SyncTestHelpers.cs @@ -142,10 +142,10 @@ public static (string[] RemainingArgs, IDisposable? Logger) SetLoggerFromArgs(st var logLevel = (LogLevel)Enum.Parse(typeof(LogLevel), extracted.RealmLogLevel!); TestHelpers.Output.WriteLine($"Setting log level to {logLevel}"); - Logger.LogLevel = logLevel; + RealmLogger.SetLogLevel(logLevel); } - Logger.AsyncFileLogger? logger = null; + RealmLogger.AsyncFileLogger? logger = null; if (!string.IsNullOrEmpty(extracted.RealmLogFile)) { if (!Process.GetCurrentProcess().ProcessName.ToLower().Contains("testhost")) @@ -153,14 +153,14 @@ public static (string[] RemainingArgs, IDisposable? Logger) SetLoggerFromArgs(st TestHelpers.Output.WriteLine($"Setting sync logger to file: {extracted.RealmLogFile}"); // We're running in a test runner, so we need to use the sync logger - Logger.Default = Logger.File(extracted.RealmLogFile!); + RealmLogger.Default = RealmLogger.File(extracted.RealmLogFile!); } else { TestHelpers.Output.WriteLine($"Setting async logger to file: {extracted.RealmLogFile}"); // We're running standalone (likely on CI), so we use the async logger - Logger.Default = logger = new Logger.AsyncFileLogger(extracted.RealmLogFile!); + RealmLogger.Default = logger = new RealmLogger.AsyncFileLogger(extracted.RealmLogFile!); } } diff --git a/Tests/Realm.Tests/Sync/SynchronizedInstanceTests.cs b/Tests/Realm.Tests/Sync/SynchronizedInstanceTests.cs index adadf62221..c2bd1259a6 100644 --- a/Tests/Realm.Tests/Sync/SynchronizedInstanceTests.cs +++ b/Tests/Realm.Tests/Sync/SynchronizedInstanceTests.cs @@ -214,8 +214,8 @@ public void GetInstanceAsync_WithOnProgressThrowing_ReportsErrorToLogs() await PopulateData(config); - var logger = new Logger.InMemoryLogger(); - Logger.Default = logger; + var logger = new RealmLogger.InMemoryLogger(); + RealmLogger.Default = logger; config = await GetIntegrationConfigAsync((string?)config.Partition); config.OnProgress = _ => throw new Exception("Exception in OnProgress"); @@ -726,9 +726,9 @@ public void RealmDispose_ClosesSessions() [Test] public void SyncTimeouts_ArePassedCorrectlyToCore() { - var logger = new Logger.InMemoryLogger(); - Logger.Default = logger; - Logger.LogLevel = LogLevel.Debug; + var logger = new RealmLogger.InMemoryLogger(); + RealmLogger.Default = logger; + RealmLogger.SetLogLevel(LogLevel.Debug); SyncTestHelpers.RunBaasTestAsync(async () => { @@ -803,7 +803,7 @@ public void SyncLogger_WhenLevelChanges_LogsAtNewLevel() } var regex = new Regex("Connection\\[\\d+] Session\\[\\d+]"); - var logger = Logger.Function((level, msg) => + var logger = RealmLogger.Function((level, msg) => { if (regex.IsMatch(msg)) { @@ -811,8 +811,8 @@ public void SyncLogger_WhenLevelChanges_LogsAtNewLevel() } }); - Logger.LogLevel = LogLevel.Info; - Logger.Default = logger; + RealmLogger.SetLogLevel(LogLevel.Info); + RealmLogger.Default = logger; SyncTestHelpers.RunBaasTestAsync(async () => { @@ -824,7 +824,7 @@ public void SyncLogger_WhenLevelChanges_LogsAtNewLevel() Assert.That(initialInfoLogs, Is.GreaterThan(0)); Assert.That(logs[LogLevel.Debug].Count, Is.EqualTo(0)); - Logger.LogLevel = LogLevel.Debug; + RealmLogger.SetLogLevel(LogLevel.Debug); realm.Write(() => { diff --git a/wrappers/realm-core b/wrappers/realm-core index fd428c3a72..cbc571b16c 160000 --- a/wrappers/realm-core +++ b/wrappers/realm-core @@ -1 +1 @@ -Subproject commit fd428c3a723a4653ee29026c1a870617d09d0e59 +Subproject commit cbc571b16c166a9f2859c2941a73fe9f061a64cf diff --git a/wrappers/src/marshalling.hpp b/wrappers/src/marshalling.hpp index b1bff56675..337b775b25 100644 --- a/wrappers/src/marshalling.hpp +++ b/wrappers/src/marshalling.hpp @@ -28,6 +28,20 @@ namespace realm::binding { +/// A struct used when marshaling of `MarshaledVector` cannot be +/// compiled, e.g. for MSVC when returning a `MarshaledVector` from +/// CPP directly, as compared to when nested within another struct. +struct TypeErasedMarshaledVector +{ + const void* items; + size_t count; + + template + static TypeErasedMarshaledVector for_marshalling(const std::vector& vector) { + return {vector.data(), vector.size()}; + } +}; + template struct MarshaledVector { diff --git a/wrappers/src/shared_realm_cs.cpp b/wrappers/src/shared_realm_cs.cpp index 8b1d6b9061..e2a6e33c00 100644 --- a/wrappers/src/shared_realm_cs.cpp +++ b/wrappers/src/shared_realm_cs.cpp @@ -48,7 +48,7 @@ using namespace realm::util; using OpenRealmCallbackT = void(void* task_completion_source, ThreadSafeReference* ref, NativeException::Marshallable ex); using RealmChangedT = void(void* managed_state_handle); using ReleaseGCHandleT = void(void* managed_handle); -using LogMessageT = void(realm_string_t message, util::Logger::Level level); +using LogMessageT = void(util::Logger::Level level, realm_string_t category_name, realm_string_t message); using MigrationCallbackT = void*(realm::SharedRealm* old_realm, realm::SharedRealm* new_realm, Schema* migration_schema, MarshaledVector, uint64_t schema_version, void* managed_migration_handle); using HandleTaskCompletionCallbackT = void(void* tcs_ptr, bool invoke_async, NativeException::Marshallable ex); using SharedSyncSession = std::shared_ptr; @@ -99,7 +99,7 @@ namespace binding { protected: void do_log(const LogCategory& category, Level level, const std::string& message) override final { - s_log_message(to_capi(message), level); + s_log_message(level, to_capi(category.get_name()), to_capi(message)); } }; } @@ -295,8 +295,31 @@ REALM_EXPORT void shared_realm_install_callbacks( LogCategory::realm.set_default_level_threshold(Logger::Level::info); } -REALM_EXPORT void shared_realm_set_log_level(Logger::Level level) { - LogCategory::realm.set_default_level_threshold(level); +REALM_EXPORT Logger::Level shared_realm_get_log_level(uint16_t* category_name_buf, size_t category_name_len) { + Utf16StringAccessor category_name(category_name_buf, category_name_len); + return LogCategory::get_category(category_name).get_default_level_threshold(); +} + +REALM_EXPORT void shared_realm_set_log_level(Logger::Level level, uint16_t* category_name_buf, size_t category_name_len) { + Utf16StringAccessor category_name(category_name_buf, category_name_len); + LogCategory::get_category(category_name).set_default_level_threshold(level); +} + +REALM_EXPORT TypeErasedMarshaledVector shared_realm_get_log_category_names() { + const auto names = LogCategory::get_category_names(); + // Declare the vector as static in order to make it a globally allocated + // and keep the vector alive beyond this call. + static std::vector result; + + // Check if it is empty before populating the result to prevent appending + // names on each invocation since the vector is global. + if (result.empty()) { + for (const auto name : names) { + result.push_back(to_capi(name)); + } + } + + return TypeErasedMarshaledVector::for_marshalling(result); } REALM_EXPORT SharedRealm* shared_realm_open(Configuration configuration, NativeException::Marshallable& ex)