From 8abdb101ac204838d9911137729ccac7c3cf3aaa Mon Sep 17 00:00:00 2001 From: Thomas Goyne Date: Fri, 26 Jul 2024 11:12:01 -0700 Subject: [PATCH] Redesign the Logger API --- Realm/ObjectServerTests/RLMSyncTestCase.mm | 1 - Realm/RLMLogger.h | 132 ++++++--- Realm/RLMLogger.mm | 203 +++++++++----- Realm/RLMLogger_Private.h | 3 +- Realm/RLMSyncManager.h | 8 +- Realm/TestUtils/RLMTestCase.m | 8 + Realm/Tests/RealmTests.mm | 89 +----- RealmSwift/Logger.swift | 72 ++--- RealmSwift/Tests/RealmTests.swift | 308 ++++++++++----------- 9 files changed, 419 insertions(+), 405 deletions(-) diff --git a/Realm/ObjectServerTests/RLMSyncTestCase.mm b/Realm/ObjectServerTests/RLMSyncTestCase.mm index edf859587d..a1b95fb7bc 100644 --- a/Realm/ObjectServerTests/RLMSyncTestCase.mm +++ b/Realm/ObjectServerTests/RLMSyncTestCase.mm @@ -613,7 +613,6 @@ - (RLMApp *)appWithId:(NSString *)appId { RLMApp *app = [RLMApp appWithConfiguration:config]; RLMSyncManager *syncManager = app.syncManager; syncManager.userAgent = self.name; - [RLMLogger setLevel:RLMLogLevelWarn forCategory:RLMLogCategorySync]; return app; } diff --git a/Realm/RLMLogger.h b/Realm/RLMLogger.h index 9e967827fe..06208d3e8f 100644 --- a/Realm/RLMLogger.h +++ b/Realm/RLMLogger.h @@ -18,6 +18,8 @@ #import +@class RLMLoggerToken; + RLM_HEADER_AUDIT_BEGIN(nullability) /// An enum representing different levels of sync-related logging that can be configured. @@ -75,7 +77,7 @@ typedef NS_ENUM(NSUInteger, RLMLogCategory) { RLMLogCategoryRealm, /// Log category for all sdk related logs. RLMLogCategorySDK, - /// Log category for all app related logs. + /// Log category for all App related logs. RLMLogCategoryApp, /// Log category for all database related logs. RLMLogCategoryStorage, @@ -115,74 +117,134 @@ typedef void (^RLMLogFunction)(RLMLogLevel level, NSString *message); /// The log function may be called from multiple threads simultaneously, and is /// responsible for performing its own synchronization if any is required. RLM_SWIFT_SENDABLE // invoked on a background thread -typedef void (^RLMLogCategoryFunction)(RLMLogLevel level, RLMLogCategory category, NSString *message) NS_REFINED_FOR_SWIFT; +typedef void (^RLMLogCategoryFunction)(RLMLogLevel level, RLMLogCategory category, NSString *message); + /** Global logger class used by all Realm components. - You can define your own logger creating an instance of `RLMLogger` and define the log function which will be - invoked whenever there is a log message. - Set this custom logger as you default logger using `setDefaultLogger`. + By default messages are logged to NSLog(), with a log level of + `RLMLogLevelInfo`. You can register an additional callback by calling + `+[RLMLogger addLogFunction:]`: - RLMLogger.defaultLogger = [[RLMLogger alloc] initWithLogFunction:^(RLMLogLevel level, NSString *category, NSString *message) { + [RLMLogger addLogFunction:^(RLMLogLevel level, NSString *category, NSString *message) { NSLog(@"Realm Log - %lu, %@, %@", (unsigned long)level, category, message); }]; - @note The default log threshold level is `RLMLogLevelInfo` for the log category `RLMLogCategoryRealm`, - and logging strings are output to Apple System Logger. + To remove the default NSLog-logger, call `[RLMLogger removeAll];` first. + + To change the log level, call `[RLMLogger setLevel:forCategory:]`. The + `RLMLogCategoryRealm` will update all log categories at once. All log + callbacks share the same log levels and are called for every category. */ @interface RLMLogger : NSObject +/// :nodoc: +- (instancetype)init NS_UNAVAILABLE; + +#pragma mark Category-based API + /** - Gets the logging threshold level used by the logger. + Registers a new logger callback function. + + The logger callback function will be invoked each time a message is logged + with a log level greater than or equal to the current log level set for the + message's category. The log function may be concurrently invoked from multiple + threads. + + This function is thread-safe and can be called at any time, including from + within other logger callbacks. It is guaranteed to work even if called + concurrently with logging operations on another thread, but whether or not + those operations are reported to the callback is left unspecified. + + This method returns a token which can be used to unregister the callback. + Unlike notification tokens, storing this token is optional. If the token is + destroyed without `invalidate` being called, it will be impossible to + unregister the callback other than with `removeAll` or `resetToDefault`. */ -@property (nonatomic) RLMLogLevel level -__attribute__((deprecated("Use `setLevel(level:category)` or `setLevel:category` instead."))); ++ (RLMLoggerToken *)addLogFunction:(RLMLogCategoryFunction)function NS_REFINED_FOR_SWIFT; -/// :nodoc: -- (instancetype)init NS_UNAVAILABLE; +/** + Removes all registered callbacks. + + This function is thread-safe. If called concurrently with logging operations + on other threads, the registered callbacks may be invoked one more time after + this function returns. + + This is the only way to remove the default NSLog logging. + */ ++ (void)removeAll; /** - Creates a logger with the associated log level and the logic function to define your own logging logic. + Resets all of the global logger state to the default. - @param level The log level to be set for the logger. - @param logFunction The log function which will be invoked whenever there is a log message. + This removes all callbacks, adds the default NSLog callback, sets the log + level to Info, and undoes the effects of calling `setDefaultLogger:`. + */ ++ (void)resetToDefault; - @note This will set the log level for the log category `RLMLogCategoryRealm`. +/** + Sets the log level for a given category. + + Some categories will also update the log level for child categories. See the + documentation for RLMLogCategory for more details. */ -- (instancetype)initWithLevel:(RLMLogLevel)level logFunction:(RLMLogFunction)logFunction -__attribute__((deprecated("Use `initWithLogFunction:` instead."))); ++ (void)setLevel:(RLMLogLevel)level forCategory:(RLMLogCategory)category NS_REFINED_FOR_SWIFT; /** - Creates a logger with a callback, which will be invoked whenever there is a log message. - - @param logFunction The log function which will be invoked whenever there is a log message. + Gets the log level for the specified category. */ -- (instancetype)initWithLogFunction:(RLMLogCategoryFunction)logFunction; ++ (RLMLogLevel)levelForCategory:(RLMLogCategory)category NS_REFINED_FOR_SWIFT; + -#pragma mark RLMLogger Default Logger API +#pragma mark Deprecated API /** - The current default logger. When setting a logger as default, this logger will replace the current default logger and will - be used whenever information must be logged. + Gets the logging threshold level used by the logger. */ -@property (class) RLMLogger *defaultLogger NS_SWIFT_NAME(shared); +@property (nonatomic) RLMLogLevel level +__attribute__((deprecated("Use `setLevel(level:category)` or `setLevel:category` instead."))); /** - Sets the gobal log level for a given category. + Creates a logger with the associated log level and the logic function to define your own logging logic. @param level The log level to be set for the logger. - @param category The log function which will be invoked whenever there is a log message. + @param logFunction The log function which will be invoked whenever there is a log message. + + @note This will set the log level for the log category `RLMLogCategoryRealm`. */ -+ (void)setLevel:(RLMLogLevel)level forCategory:(RLMLogCategory)category NS_REFINED_FOR_SWIFT; +- (instancetype)initWithLevel:(RLMLogLevel)level logFunction:(RLMLogFunction)logFunction +__attribute__((deprecated("Use `+[Logger addLogFunction:]` instead."))); /** - Gets the global log level for the specified category. + The current default logger. When setting a logger as default, this logger will + replace the current default logger and will be used whenever information must + be logged. - @param category The log category which we need the level. - @returns The log level for the specified category -*/ -+ (RLMLogLevel)levelForCategory:(RLMLogCategory)category NS_REFINED_FOR_SWIFT; + Overriding the default logger will result in callbacks registered with + `addLogFunction:` never being invoked. + */ +@property (class) RLMLogger *defaultLogger NS_SWIFT_NAME(shared) +__attribute__((deprecated("Use `+[Logger addLogFunction:]` instead."))); +@end +/** + A token which can be used to remove logger callbacks. + + This token only needs to be stored if you wish to be able to remove individual + callbacks. If the token is destroyed without `invalidate` being called the + callback will not be removed. + */ +RLM_SWIFT_SENDABLE RLM_FINAL +@interface RLMLoggerToken : NSObject +/** + Removes the associated logger callback. + + This function is thread-safe and idempotent. Calling it multiple times or from + multiple threads at once is not an error. If called concurrently with logging + operations on another thread, the associated callback may be called one more + time per thread after this function returns. + */ +- (void)invalidate; @end RLM_HEADER_AUDIT_END(nullability) diff --git a/Realm/RLMLogger.mm b/Realm/RLMLogger.mm index 190089501d..1f523a8d39 100644 --- a/Realm/RLMLogger.mm +++ b/Realm/RLMLogger.mm @@ -22,8 +22,6 @@ #import -typedef void (^RLMLoggerFunction)(RLMLogLevel level, RLMLogCategory category, NSString *message); - using namespace realm; using Logger = realm::util::Logger; using Level = Logger::Level; @@ -60,38 +58,38 @@ static RLMLogLevel logLevelForLevel(Level logLevel) { REALM_UNREACHABLE(); // Unrecognized log level. } -static NSString* levelPrefix(Level logLevel) { +static NSString* levelPrefix(RLMLogLevel logLevel) { switch (logLevel) { - case Level::off: return @""; - case Level::all: return @""; - case Level::trace: return @"Trace"; - case Level::debug: return @"Debug"; - case Level::detail: return @"Detail"; - case Level::info: return @"Info"; - case Level::error: return @"Error"; - case Level::warn: return @"Warning"; - case Level::fatal: return @"Fatal"; + case RLMLogLevelOff: return @""; + case RLMLogLevelAll: return @""; + case RLMLogLevelTrace: return @"Trace"; + case RLMLogLevelDebug: return @"Debug"; + case RLMLogLevelDetail: return @"Detail"; + case RLMLogLevelInfo: return @"Info"; + case RLMLogLevelError: return @"Error"; + case RLMLogLevelWarn: return @"Warning"; + case RLMLogLevelFatal: return @"Fatal"; } REALM_UNREACHABLE(); // Unrecognized log level. } static LogCategory& categoryForLogCategory(RLMLogCategory logCategory) { switch (logCategory) { - case RLMLogCategoryRealm: return LogCategory::realm; - case RLMLogCategorySDK: return LogCategory::sdk; - case RLMLogCategoryApp: return LogCategory::app; - case RLMLogCategoryStorage: return LogCategory::storage; - case RLMLogCategoryStorageTransaction: return LogCategory::transaction; - case RLMLogCategoryStorageQuery: return LogCategory::query; - case RLMLogCategoryStorageObject: return LogCategory::object; + case RLMLogCategoryRealm: return LogCategory::realm; + case RLMLogCategorySDK: return LogCategory::sdk; + case RLMLogCategoryApp: return LogCategory::app; + case RLMLogCategoryStorage: return LogCategory::storage; + case RLMLogCategoryStorageTransaction: return LogCategory::transaction; + case RLMLogCategoryStorageQuery: return LogCategory::query; + case RLMLogCategoryStorageObject: return LogCategory::object; case RLMLogCategoryStorageNotification: return LogCategory::notification; - case RLMLogCategorySync: return LogCategory::sync; - case RLMLogCategorySyncClient: return LogCategory::client; - case RLMLogCategorySyncClientSession: return LogCategory::session; + case RLMLogCategorySync: return LogCategory::sync; + case RLMLogCategorySyncClient: return LogCategory::client; + case RLMLogCategorySyncClientSession: return LogCategory::session; case RLMLogCategorySyncClientChangeset: return LogCategory::changeset; - case RLMLogCategorySyncClientNetwork: return LogCategory::network; - case RLMLogCategorySyncClientReset: return LogCategory::reset; - case RLMLogCategorySyncServer: return LogCategory::server; + case RLMLogCategorySyncClientNetwork: return LogCategory::network; + case RLMLogCategorySyncClientReset: return LogCategory::reset; + case RLMLogCategorySyncServer: return LogCategory::server; }; REALM_UNREACHABLE(); } @@ -136,28 +134,121 @@ static RLMLogCategory logCategoryForCategory(const LogCategory& category) { return find(category); } -struct CocoaLogger : public Logger { +struct DynamicLogger : Logger { + RLMUnfairMutex _mutex; + NSArray *_logFunctions; + void do_log(const LogCategory& category, Level level, const std::string& message) override { - NSLog(@"%@:%s %@", levelPrefix(level), category.get_name().c_str(), RLMStringDataToNSString(message)); + NSArray *loggers; + { + std::lock_guard lock(_mutex); + loggers = _logFunctions; + } + if (loggers.count == 0) { + return; + } + + @autoreleasepool { + NSString *nsMessage = RLMStringDataToNSString(message); + RLMLogCategory rlmCategory = logCategoryForCategory(category); + RLMLogLevel rlmLevel = logLevelForLevel(level); + for (RLMLogCategoryFunction fn : loggers) { + fn(rlmLevel, rlmCategory, nsMessage); + } + } } -}; +} s_dynamic_logger; class CustomLogger : public Logger { public: - RLMLoggerFunction function; - void do_log(const LogCategory& category, Level level, const std::string& message) override { + RLMLogFunction function; + void do_log(const LogCategory&, Level level, const std::string& message) override { @autoreleasepool { - function(logLevelForLevel(level), logCategoryForCategory(category), RLMStringDataToNSString(message)); + function(logLevelForLevel(level), RLMStringDataToNSString(message)); } } }; } // anonymous namespace +@implementation RLMLoggerToken { + RLMLogCategoryFunction _function; +} + +- (instancetype)initWithFunction:(RLMLogCategoryFunction)function { + if (self = [super init]) { + _function = function; + } + return self; +} + +- (void)invalidate { + std::lock_guard lock(s_dynamic_logger._mutex); + if (!_function) { + return; + } + auto& functions = s_dynamic_logger._logFunctions; + functions = [functions filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"SELF != %@", _function]]; + _function = nil; +} + +@end + @implementation RLMLogger { std::shared_ptr _logger; } -typedef void(^LoggerBlock)(RLMLogLevel level, NSString *message); +#pragma mark - Dynamic category-based API + ++ (void)initialize { + [self resetToDefault]; +} + ++ (void)resetToDefault { + { + std::lock_guard lock(s_dynamic_logger._mutex); + static RLMLogCategoryFunction defaultLogger = ^(RLMLogLevel level, RLMLogCategory category, + NSString *message) { + NSLog(@"%@:%s %@", levelPrefix(level), categoryForLogCategory(category).get_name().c_str(), message); + }; + s_dynamic_logger._logFunctions = @[defaultLogger]; + s_dynamic_logger.set_level_threshold(LogCategory::realm, Level::info); + } + // Use a custom no-op deleter because our logger is statically allocated and + // shouldn't actually be deleted when there's no references to it + Logger::set_default_logger(std::shared_ptr(&s_dynamic_logger, [](Logger *) {})); +} + ++ (RLMLoggerToken *)addLogFunction:(RLMLogCategoryFunction)function { + { + std::lock_guard lock(s_dynamic_logger._mutex); + // We construct a new array each time rather than using a mutable array + // so that do_log() can just acquire the pointer under lock without + // having to worry about the array being mutated on another thread + auto& functions = s_dynamic_logger._logFunctions; + if (functions.count) { + functions = [functions arrayByAddingObject:function]; + } + else { + functions = @[function]; + } + } + return [[RLMLoggerToken alloc] initWithFunction:function]; +} + ++ (void)removeAll { + std::lock_guard lock(s_dynamic_logger._mutex); + s_dynamic_logger._logFunctions = nil; +} + ++ (void)setLevel:(RLMLogLevel)level forCategory:(RLMLogCategory)category { + s_dynamic_logger.set_level_threshold(categoryForLogCategory(category), levelForLogLevel(level)); +} + ++ (RLMLogLevel)levelForCategory:(RLMLogCategory)category { + return logLevelForLevel(s_dynamic_logger.get_level_threshold(categoryForLogCategory(category))); +} + +#pragma mark - Deprecated API - (RLMLogLevel)level { return logLevelForLevel(_logger->get_level_threshold()); @@ -167,12 +258,6 @@ - (void)setLevel:(RLMLogLevel)level { _logger->set_level_threshold(levelForLogLevel(level)); } -+ (void)initialize { - auto defaultLogger = std::make_shared(); - defaultLogger->set_level_threshold(LogCategory::realm, Level::info); - Logger::set_default_logger(defaultLogger); -} - - (instancetype)initWithLogger:(std::shared_ptr)logger { if (self = [self init]) { self->_logger = logger; @@ -185,33 +270,21 @@ - (instancetype)initWithLevel:(RLMLogLevel)level if (self = [super init]) { auto logger = std::make_shared(); logger->set_level_threshold(levelForLogLevel(level)); - logger->function = ^(RLMLogLevel level, RLMLogCategory, NSString *message) { - logFunction(level, message); - }; - self->_logger = logger; - } - return self; -} - -- (instancetype)initWithLogFunction:(RLMLogCategoryFunction)logFunction { - if (self = [super init]) { - auto logger = std::make_shared(); logger->function = logFunction; self->_logger = logger; } return self; } -+ (void)setLevel:(RLMLogLevel)level forCategory:(RLMLogCategory)category { - auto defaultLogger = Logger::get_default_logger(); - defaultLogger->set_level_threshold(categoryForLogCategory(category).get_name(), levelForLogLevel(level)); ++ (instancetype)defaultLogger { + return [[RLMLogger alloc] initWithLogger:Logger::get_default_logger()]; } -+ (RLMLogLevel)levelForCategory:(RLMLogCategory)category { - auto defaultLogger = Logger::get_default_logger(); - return logLevelForLevel(defaultLogger->get_level_threshold(categoryForLogCategory(category).get_name())); ++ (void)setDefaultLogger:(RLMLogger *)logger { + Logger::set_default_logger(logger->_logger); } + #pragma mark Testing + (NSArray *)allCategories { @@ -229,18 +302,10 @@ void RLMTestLog(RLMLogCategory category, RLMLogLevel level, const char *message) levelForLogLevel(level), "%1", message); } - -#pragma mark Global Logger Setter - -+ (instancetype)defaultLogger { - return [[RLMLogger alloc] initWithLogger:Logger::get_default_logger()]; -} - -+ (void)setDefaultLogger:(RLMLogger *)logger { - Logger::set_default_logger(logger->_logger); -} @end +#pragma mark - Internal logging functions + void RLMLog(RLMLogLevel logLevel, NSString *format, ...) { auto level = levelForLogLevel(logLevel); auto logger = Logger::get_default_logger(); @@ -253,14 +318,6 @@ void RLMLog(RLMLogLevel logLevel, NSString *format, ...) { } } -void RLMLogDeferred(RLMLogLevel logLevel, NSString *(NS_NOESCAPE ^message)()) { - auto level = levelForLogLevel(logLevel); - auto logger = Logger::get_default_logger(); - if (logger->would_log(LogCategory::sdk, level)) { - logger->log(LogCategory::sdk, level, "%1", message().UTF8String); - } -} - void RLMLogRaw(RLMLogLevel logLevel, NSString *message) { auto level = levelForLogLevel(logLevel); auto logger = Logger::get_default_logger(); diff --git a/Realm/RLMLogger_Private.h b/Realm/RLMLogger_Private.h index d159fc5ba6..1cd204ce6f 100644 --- a/Realm/RLMLogger_Private.h +++ b/Realm/RLMLogger_Private.h @@ -40,8 +40,7 @@ FOUNDATION_EXTERN void RLMTestLog(RLMLogCategory category, RLMLogLevel level, co /// Logger function for operations within the SDK, to be used from obj-c code. void RLMLog(RLMLogLevel level, NSString *format, ...); -// Helpers for the Swift Logger.log() function +// Helper for the Swift Logger.log() function FOUNDATION_EXTERN void RLMLogRaw(RLMLogLevel level, NSString *message); -FOUNDATION_EXTERN void RLMLogDeferred(RLMLogLevel level, NSString *(NS_NOESCAPE ^message)(void)); RLM_HEADER_AUDIT_END(nullability) diff --git a/Realm/RLMSyncManager.h b/Realm/RLMSyncManager.h index 1dee453d0d..809d6c3f5d 100644 --- a/Realm/RLMSyncManager.h +++ b/Realm/RLMSyncManager.h @@ -105,25 +105,25 @@ __attribute__((deprecated("This property is not used for anything"))); The logging threshold which newly opened synced Realms will use. Defaults to `RLMSyncLogLevelInfo`. - By default logging strings are output to Apple System Logger. Set `logger` to + By default logging strings are output to NSLog. Set `logger` to perform custom logging logic instead. @warning This property must be set before any synced Realms are opened. Setting it after opening any synced Realm will do nothing. */ @property (atomic) RLMSyncLogLevel logLevel -__attribute__((deprecated("Use `RLMLogger.default.level`/`Logger.shared.level` to set/get the default logger threshold level."))); +__attribute__((deprecated("Use `Logger.set(level: level, category: Category.Sync.all)` to set the log level for sync operations."))); /** The function which will be invoked whenever the sync client has a log message. - If nil, log strings are output to Apple System Logger instead. + If nil, log strings are output to RLMLogger instead. @warning This property must be set before any synced Realms are opened. Setting it after opening any synced Realm will do nothing. */ @property (atomic, nullable) RLMSyncLogFunction logger -__attribute__((deprecated("Use `RLMLogger.default`/`Logger.shared` to set/get the default logger."))); +__attribute__((deprecated("Use `Logger.add(function:)` and filter messages by category."))); /** The name of the HTTP header to send authorization data in when making requests to Atlas App Services which has diff --git a/Realm/TestUtils/RLMTestCase.m b/Realm/TestUtils/RLMTestCase.m index b9ffe5ad73..f7c8b30685 100644 --- a/Realm/TestUtils/RLMTestCase.m +++ b/Realm/TestUtils/RLMTestCase.m @@ -96,6 +96,9 @@ + (void)setUp { // resetting the simulator [NSFileManager.defaultManager createDirectoryAtURL:RLMDefaultRealmURL().URLByDeletingLastPathComponent withIntermediateDirectories:YES attributes:nil error:nil]; + + [RLMLogger resetToDefault]; + [RLMLogger setLevel:RLMLogLevelWarn forCategory:RLMLogCategorySync]; } // This ensures the shared schema is initialized outside of of a test case, @@ -116,6 +119,11 @@ @implementation RLMTestCase { dispatch_queue_t _bgQueue; } +- (void)tearDown { + [RLMLogger resetToDefault]; + [RLMLogger setLevel:RLMLogLevelWarn forCategory:RLMLogCategorySync]; +} + - (void)deleteFiles { // Clear cache [self resetRealmState]; diff --git a/Realm/Tests/RealmTests.mm b/Realm/Tests/RealmTests.mm index e095b28bcb..d74a590633 100644 --- a/Realm/Tests/RealmTests.mm +++ b/Realm/Tests/RealmTests.mm @@ -2949,86 +2949,37 @@ - (void)testDeleteOpenRealmFile { @end @interface RLMLoggerTests : RLMTestCase -@property (nonatomic, strong) RLMLogger *logger; @end @implementation RLMLoggerTests -- (void)setUp { - _logger = RLMLogger.defaultLogger; -} -- (void)tearDown { - RLMLogger.defaultLogger = _logger; -} - - (void)testSetDefaultLogLevel { __block NSMutableString *logs = [[NSMutableString alloc] init]; - RLMLogCategory category = RLMLogCategoryRealm; - RLMLogger *logger = [[RLMLogger alloc] initWithLogFunction:^(RLMLogLevel level, RLMLogCategory category, NSString *message) { - [logs appendFormat:@" %@ %lu %lu %@", [NSDate date], (unsigned long)category, level, message]; + [RLMLogger removeAll]; + [RLMLogger addLogFunction:^(RLMLogLevel level, RLMLogCategory, NSString *message) { + [logs appendFormat:@"%d %@", (int)level, message]; }]; - RLMLogger.defaultLogger = logger; - [RLMLogger setLevel:RLMLogLevelAll forCategory:category]; + [RLMLogger setLevel:RLMLogLevelAll forCategory:RLMLogCategoryRealm]; @autoreleasepool { [RLMRealm defaultRealm]; } - XCTAssertEqual([RLMLogger levelForCategory:category], RLMLogLevelAll); + XCTAssertEqual([RLMLogger levelForCategory:RLMLogCategoryRealm], RLMLogLevelAll); XCTAssertTrue([logs containsString:@"5 DB:"]); // Detail XCTAssertTrue([logs containsString:@"7 DB:"]); // Trace - [logs setString: @""]; - [RLMLogger setLevel:RLMLogLevelDetail forCategory:category]; - @autoreleasepool { [RLMRealm defaultRealm]; } - XCTAssertEqual([RLMLogger levelForCategory:category], RLMLogLevelDetail); - XCTAssertTrue([logs containsString:@"5 DB:"]); // Detail - XCTAssertFalse([logs containsString:@"7 DB:"]); // Trace -} - -- (void)testDefaultLogger { - __block NSMutableString *logs = [[NSMutableString alloc] init]; - RLMLogCategory category = RLMLogCategoryRealm; - RLMLogger *logger = [[RLMLogger alloc] initWithLogFunction:^(RLMLogLevel level, RLMLogCategory category, NSString *message) { - [logs appendFormat:@" %@ %lu %lu %@", [NSDate date], (unsigned long)category, level, message]; - }]; - RLMLogger.defaultLogger = logger; - [RLMLogger setLevel:RLMLogLevelOff forCategory:category]; - XCTAssertEqual([RLMLogger levelForCategory:category], RLMLogLevelOff); - - @autoreleasepool { [RLMRealm defaultRealm]; } - XCTAssertTrue([logs length] == 0); - - // Test LogLevel Detail - [RLMLogger setLevel:RLMLogLevelDetail forCategory:category]; - @autoreleasepool { [RLMRealm defaultRealm]; } - XCTAssertTrue([logs length] > 0); - XCTAssertTrue([logs containsString:@"5 DB:"]); // Detail - XCTAssertFalse([logs containsString:@"7 DB:"]); // Trace - - // Test LogLevel All - [RLMLogger setLevel:RLMLogLevelAll forCategory:category]; - @autoreleasepool { [RLMRealm defaultRealm]; } - XCTAssertTrue([logs length] > 0); - XCTAssertTrue([logs containsString:@"5 DB:"]); // Detail - XCTAssertTrue([logs containsString:@"7 DB:"]); // Trace - - [logs setString: @""]; - // Init Custom Logger - RLMLogger.defaultLogger = [[RLMLogger alloc] initWithLogFunction:^(RLMLogLevel level, RLMLogCategory category, NSString * message) { - [logs appendFormat:@" %@ %lu %lu %@", [NSDate date], (unsigned long)category, level, message]; - }]; - [RLMLogger setLevel:RLMLogLevelDebug forCategory:category]; - XCTAssertEqual([RLMLogger levelForCategory:category], RLMLogLevelDebug); + [logs setString:@""]; + [RLMLogger setLevel:RLMLogLevelDetail forCategory:RLMLogCategoryRealm]; @autoreleasepool { [RLMRealm defaultRealm]; } + XCTAssertEqual([RLMLogger levelForCategory:RLMLogCategoryRealm], RLMLogLevelDetail); XCTAssertTrue([logs containsString:@"5 DB:"]); // Detail XCTAssertFalse([logs containsString:@"7 DB:"]); // Trace } - (void)testCustomLoggerLogMessage { __block NSMutableString *logs = [[NSMutableString alloc] init]; - RLMLogCategory category = RLMLogCategoryRealm; - RLMLogger *logger = [[RLMLogger alloc] initWithLogFunction:^(RLMLogLevel level, RLMLogCategory category, NSString * message) { - [logs appendFormat:@" %@ %lu %lu %@", [NSDate date], (unsigned long)category, level, message]; + [RLMLogger removeAll]; + [RLMLogger addLogFunction:^(RLMLogLevel, RLMLogCategory category, NSString *message) { + [logs appendFormat:@"%d %@", (int)category, message]; }]; - RLMLogger.defaultLogger = logger; - [RLMLogger setLevel:RLMLogLevelDebug forCategory:category]; + [RLMLogger setLevel:RLMLogLevelDebug forCategory:RLMLogCategorySDK]; RLMLog(RLMLogLevelInfo, @"%@ IMPORTANT INFO %i", @"TEST:", 0); RLMLog(RLMLogLevelTrace, @"IMPORTANT TRACE"); @@ -3101,25 +3052,15 @@ - (void)testOldDefaultLogger { @end @interface RLMMetricsTests : RLMTestCase -@property (nonatomic, strong) RLMLogger *logger; @end @implementation RLMMetricsTests -- (void)setUp { - _logger = RLMLogger.defaultLogger; -} -- (void)tearDown { - RLMLogger.defaultLogger = _logger; -} - - (void)testSyncConnectionMetrics { __block NSMutableString *logs = [[NSMutableString alloc] init]; - RLMLogCategory category = RLMLogCategoryRealm; - RLMLogger *logger = [[RLMLogger alloc] initWithLogFunction:^(RLMLogLevel level, RLMLogCategory category, NSString * message) { - [logs appendFormat:@" %@ %lu %lu %@", [NSDate date], (unsigned long)category, level, message]; + [RLMLogger addLogFunction:^(RLMLogLevel, RLMLogCategory, NSString *message) { + [logs appendString:message]; }]; - RLMLogger.defaultLogger = logger; - [RLMLogger setLevel:RLMLogLevelAll forCategory:category]; + [RLMLogger setLevel:RLMLogLevelAll forCategory:RLMLogCategoryRealm]; RLMApp *app = [RLMApp appWithId:@"test-id"]; // We don't even need the login to succeed, we only want for the logger // to log the values on device info after trying to login. diff --git a/RealmSwift/Logger.swift b/RealmSwift/Logger.swift index 115b23eb6a..4db4241684 100644 --- a/RealmSwift/Logger.swift +++ b/RealmSwift/Logger.swift @@ -67,48 +67,6 @@ extension Logger { internal static func log(_ level: LogLevel, _ message: String) { RLMLogRaw(level, message) } - internal static func log(_ level: LogLevel, _ message: @autoclosure () -> DefaultStringInterpolation) { - RLMLogDeferred(level) { message().description } - } - - /** - Creates a logger with the associated log level, and a logic function to define your own logging logic. - - ```swift - let logger = Logger(level: .info, category: Category.All, logFunction: { level, category, message in - print("\(category.rawValue) - \(level): \(message)") - }) - ``` - - - parameter level: The log level to be set for the logger. - - parameter function: The log function which will be invoked whenever there is a log message. - - - note: This will set the specified log level for the log category `Category.realm`. - */ - @available(*, deprecated, message: "Use init(function:)") - public convenience init(level: LogLevel, function: @escaping @Sendable (LogLevel, LogCategory, String) -> Void) { - self.init(logFunction: { level, category, message in - function(level, ObjectiveCSupport.convert(value: category), message) - }) - Logger.setLogLevel(level, for: Category.realm) - } - - /** - Creates a logger with a callback, which will be invoked whenever there is a log message. - - ```swift - let logger = Logger(function: { level, category, message in - print("\(category.rawValue) - \(level): \(message)") - }) - ``` - - - parameter function: The log function which will be invoked whenever there is a log message. - */ - public convenience init(function: @escaping @Sendable (LogLevel, LogCategory, String) -> Void) { - self.init(logFunction: { level, category, message in - function(level, ObjectiveCSupport.convert(value: category), message) - }) - } /** Sets the global log level for a given log category. @@ -119,7 +77,7 @@ extension Logger { - note:By setting the log level of a category, it will set all its subcategories log level as well. - SeeAlso: `LogCategory` */ - public static func setLogLevel(_ level: LogLevel, for category: LogCategory = Category.realm) { + public static func set(level: LogLevel, for category: LogCategory = Category.realm) { Logger.__setLevel(level, for: ObjectiveCSupport.convert(value: category)) } @@ -131,9 +89,18 @@ extension Logger { - returns: The `LogLevel` for the given category. - SeeAlso: `LogCategory` */ - public static func logLevel(for category: LogCategory) -> LogLevel { + public static func logLevel(for category: LogCategory = Category.realm) -> LogLevel { Logger.__level(for: ObjectiveCSupport.convert(value: category)) } + + public typealias LogCallback = @Sendable (LogLevel, LogCategory, String) -> Void + + @discardableResult + public static func addLogger(function: @escaping LogCallback) -> RLMLoggerToken { + Self.__addLogFunction { level, category, message in + function(level, ObjectiveCSupport.convert(value: category), message) + } + } } /// Defines a log category for the Realm `Logger`. @@ -167,12 +134,12 @@ public protocol LogCategory: Sendable { └─► Sdk ``` */ -public enum Category: String, LogCategory { - /// Top level log category for Realm, updating this category level would set all other subcategories too. +public enum Category: String, LogCategory, CaseIterable { + /// Top level log category for all messages. Setting the log level for this category updates all other categories as well. case realm = "Realm" - /// Log category for all sdk related logs. + /// Log category for things logged by the Realm Swift SDK. case sdk = "Realm.SDK" - /// Log category for all app related logs. + /// Log category for the App type. This includes al HTTP(s) requests made to Atlas, but does not include sync. case app = "Realm.App" /** @@ -187,7 +154,7 @@ public enum Category: String, LogCategory { └─► Notification ``` */ - public enum Storage: String, LogCategory { + public enum Storage: String, LogCategory, CaseIterable { /// Log category for all database related logs. case all = "Realm.Storage" /// Log category for all database transaction related logs. @@ -214,7 +181,7 @@ public enum Category: String, LogCategory { └─► Server ``` */ - public enum Sync: String, LogCategory { + public enum Sync: String, LogCategory, CaseIterable { /// Log category for all sync related logs. case all = "Realm.Sync" /// Log category for all sync server related logs. @@ -232,7 +199,7 @@ public enum Category: String, LogCategory { └─► Reset ``` */ - public enum Client: String, LogCategory { + public enum Client: String, LogCategory, CaseIterable { /// Log category for all sync client related logs. case all = "Realm.Sync.Client" /// Log category for all sync client session related logs. @@ -247,8 +214,7 @@ public enum Category: String, LogCategory { } } -internal extension ObjectiveCSupport { - +public extension ObjectiveCSupport { /// Converts a Swift category `LogCategory` to an Objective-C `RLMLogCategory. /// - Parameter value: The `LogCategory`. /// - Returns: Conversion of `value` to its Objective-C representation. diff --git a/RealmSwift/Tests/RealmTests.swift b/RealmSwift/Tests/RealmTests.swift index 0dfa1226c9..b12a0a3afb 100644 --- a/RealmSwift/Tests/RealmTests.swift +++ b/RealmSwift/Tests/RealmTests.swift @@ -1947,94 +1947,189 @@ extension LogLevel { @available(macOS 12.0, watchOS 8.0, iOS 15.0, tvOS 15.0, macCatalyst 15.0, *) class LoggerTests: TestCase, @unchecked Sendable { - var oldLogger: Logger! let logs = Locked("") override func setUp() { - oldLogger = Logger.shared let logs = self.logs - Logger.shared = Logger(function: { level, category, message in + Logger.removeAll() + Logger.addLogger { level, category, message in logs.withLock { $0 += "\(Date.now) \(category.rawValue)[\(level.logLevel)]: \(message)\n" } - }) - } - - override func tearDown() { - Logger.shared = oldLogger + } } func assertContains(_ str: String, _ expected: String, line: UInt = #line) { XCTAssert(str.contains(expected), "\"\(str)\" should contain \"\(expected)\"", line: line) } - func testSetDefaultLogLevel() throws { - Logger.setLogLevel(.off, for: Category.realm) + func testAllCategoriesAreMapped() { + for category in Logger.allCategories() { + XCTAssertNotNil(categoryfromString(category), "LogCategory `\(category)` not added to the Category enum.") + XCTAssertEqual(categoryfromString(category)?.rawValue, category) + } + } - try autoreleasepool { _ = try Realm() } - XCTAssertTrue(logs.value.isEmpty) + let logLevels: [LogLevel] = [.off, .fatal, .error, .warn, .info, .detail, .debug, .trace, .all] - Logger.setLogLevel(.all, for: Category.realm) - try autoreleasepool { _ = try Realm() } // We should be getting logs after changing the log level - XCTAssertEqual(Logger.logLevel(for: Category.realm), .all) - assertContains(logs.value, "[Details]: DB:") - assertContains(logs.value, "[Trace]: DB:") + var allCategories: [any LogCategory] { + (Category.allCases as [any LogCategory]) + allStorageCategories + allSyncCategories + } + var allStorageCategories: [any LogCategory] { + Category.Storage.allCases + } + var allSyncCategories: [any LogCategory] { + (Category.Sync.allCases as [any LogCategory]) + allSyncClientCategories + } + var allSyncClientCategories: [any LogCategory] { + Category.Sync.Client.allCases } - func testSetDefaultLogger() throws { - Logger.setLogLevel(.off, for: Category.realm) - XCTAssertEqual(Logger.logLevel(for: Category.realm), .off) - try autoreleasepool { _ = try Realm() } - XCTAssertTrue(logs.value.isEmpty) + func testSetLogLevels() { + for level in logLevels { + for category in allCategories { + Logger.set(level: level, for: category) + XCTAssertEqual(level, Logger.logLevel(for: category)) + } + } + } - // Info - Logger.setLogLevel(.detail, for: Category.realm) - try autoreleasepool { _ = try Realm() } + func testCategoryLogLevelInheritance() { + for level in logLevels { + // realm category should update the log level for all categories + Logger.set(level: .off) + Logger.set(level: level, for: Category.realm) + for category in allCategories { + XCTAssertEqual(level, Logger.logLevel(for: category)) + } - assertContains(logs.value, "[Details]: DB:") + // Each other category should update its children but not other categories + Logger.set(level: .off) + Logger.set(level: level, for: Category.Storage.all) + for category in allStorageCategories { + XCTAssertEqual(level, Logger.logLevel(for: category)) + } - // Trace - logs.wrappedValue = "" - Logger.setLogLevel(.trace, for: Category.realm) - try autoreleasepool { _ = try Realm() } + XCTAssertEqual(.off, Logger.logLevel(for: Category.realm)) + XCTAssertEqual(.off, Logger.logLevel(for: Category.sdk)) + XCTAssertEqual(.off, Logger.logLevel(for: Category.app)) + for category in allSyncCategories { + XCTAssertEqual(.off, Logger.logLevel(for: category)) + } - assertContains(logs.value, "[Trace]: DB:") + Logger.set(level: .off) + Logger.set(level: level, for: Category.Sync.all) + for category in allSyncCategories { + XCTAssertEqual(level, Logger.logLevel(for: category)) + } - // Detail - logs.wrappedValue = "" - Logger.setLogLevel(.detail, for: Category.realm) - try autoreleasepool { _ = try Realm() } + XCTAssertEqual(.off, Logger.logLevel(for: Category.realm)) + XCTAssertEqual(.off, Logger.logLevel(for: Category.sdk)) + XCTAssertEqual(.off, Logger.logLevel(for: Category.app)) + for category in allStorageCategories { + XCTAssertEqual(.off, Logger.logLevel(for: category)) + } - assertContains(logs.value, "[Details]: DB:") - XCTAssertFalse(logs.value.contains("[Trace]: DB:")) + Logger.set(level: .off) + Logger.set(level: level, for: Category.Sync.Client.all) + for category in allSyncClientCategories { + XCTAssertEqual(level, Logger.logLevel(for: category)) + } - logs.wrappedValue = "" - let logs = self.logs - Logger.shared = Logger(function: { level, _, message in - logs.withLock({ $0 += "\(Date.now) \(level.logLevel) \(message)" }) - }) - Logger.setLogLevel(.trace, for: Category.realm) - XCTAssertEqual(Logger.logLevel(for: Category.realm), .trace) - try autoreleasepool { _ = try Realm() } - assertContains(logs.value, "Details DB:") - assertContains(logs.value, "Trace DB:") + XCTAssertEqual(.off, Logger.logLevel(for: Category.realm)) + XCTAssertEqual(.off, Logger.logLevel(for: Category.sdk)) + XCTAssertEqual(.off, Logger.logLevel(for: Category.app)) + for category in allStorageCategories { + XCTAssertEqual(.off, Logger.logLevel(for: category)) + } + } + } + + // Test that we're actually setting the log level in core by logging messages + // that should and shouldn't be passed to the callback at each level + func testLogLevelsAreActuallyApplied() { + for category in allCategories { + let rlmCategory = ObjectiveCSupport.convert(value: category) + for i in 0..<(logLevels.count - 1) { + Logger.set(level: logLevels[i], for: category) + RLMTestLog(rlmCategory, logLevels[i], "message") + XCTAssertFalse(logs.value.isEmpty) + logs.value = "" + + RLMTestLog(rlmCategory, logLevels[i + 1], "message") + XCTAssertTrue(logs.value.isEmpty) + logs.value = "" + } + } + } + + func testDynamicallyUpdateLogLevel() throws { + let realm = try Realm() + logs.value = "" + + Logger.set(level: .off, for: Category.Storage.transaction) + try realm.write {} + XCTAssert(logs.value.isEmpty) + + Logger.set(level: .all, for: Category.Storage.transaction) + try realm.write {} + assertContains(logs.value, "Realm.Storage.Transaction[Trace]: DB") + assertContains(logs.value, "Realm.Storage.Transaction[Debug]: DB") + + logs.value = "" + Logger.set(level: .debug, for: Category.Storage.transaction) + try realm.write {} + XCTAssertFalse(logs.value.contains("Realm.Storage.Transaction[Trace]: DB")) + assertContains(logs.value, "Realm.Storage.Transaction[Debug]: DB") + } + + func testDynamicallyAddAndRemoveLoggers() throws { + Logger.set(level: .all, for: Category.Storage.transaction) + let realm = try Realm() + logs.value = "" + + try realm.write {} + assertContains(logs.value, "Realm.Storage.Transaction[Trace]: DB") + logs.value = "" + + Logger.removeAll() + try realm.write {} + XCTAssert(logs.value.isEmpty) + + let called = Locked(false) + let logFn: Logger.LogCallback = { (_, category, _) in + XCTAssertEqual(category.rawValue, Category.Storage.transaction.rawValue) + called.value = true + } + + let token = Logger.addLogger(function: logFn) + try realm.write {} + XCTAssert(called.value) + + called.value = false + token.invalidate() + try realm.write {} + XCTAssertFalse(called.value) } @available(*, deprecated) func testOldSetDefaultLogLevel() throws { + Logger.shared = .init(level: .off) { level, message in + self.logs.value += "\(level.logLevel): \(message)" + } try autoreleasepool { _ = try Realm() } XCTAssertTrue(logs.wrappedValue.isEmpty) Logger.shared.level = .all try autoreleasepool { _ = try Realm() } // We should be getting logs after changing the log level XCTAssertEqual(Logger.shared.level, .all) - assertContains(logs.value, "[Details]: DB:") - assertContains(logs.value, "[Trace]: DB:") + assertContains(logs.value, "Details: DB:") + assertContains(logs.value, "Trace: DB:") } @available(*, deprecated) func testOldDefaultLogger() throws { var logs: String = "" let logger = Logger(level: .off) { level, message in - logs += "\(Date.now) \(level.logLevel) \(message)" + logs += "\(level.logLevel) \(message)" } Logger.shared = logger @@ -2078,119 +2173,6 @@ class LoggerTests: TestCase, @unchecked Sendable { assertContains(logs, "Trace DB:") } - // Core defines the different categories in runtime, forcing the SDK to define the categories again. - // This test validates that we have added new defined categories to the Categories enum and/or - // child categories - func testAllCategoriesWatchDog() throws { - for category in Logger.allCategories() { - XCTAssertNotNil(categoryfromString(category), "LogCategory `\(category)` not added to the Category enum.") - XCTAssertEqual(categoryfromString(category)?.rawValue, category) - } - } - - func testLogLevelForCategories() throws { - Logger.setLogLevel(.off, for: Category.realm) - XCTAssertEqual(Logger.logLevel(for: Category.realm), .off) - - for category in Logger.allCategories() { - let categoryEnum = categoryfromString(category) - XCTAssertNotNil(categoryEnum, "LogCategory `\(category)` not added to the Category enum.") - - Logger.setLogLevel(.trace, for: categoryEnum!) - XCTAssertEqual(Logger.logLevel(for: categoryEnum!), .trace) - XCTAssertNotEqual(Logger.logLevel(for: categoryEnum!), .all) - } - } - - /// This test works because `get_category_names()` returns categories from parent to children. - func testShouldNotLogParentOrRelatedCategory() throws { - Logger.setLogLevel(.off, for: Category.realm) - XCTAssertEqual(Logger.logLevel(for: Category.realm), .off) - - let categories = Logger.allCategories() - for (index, category) in categories.enumerated() { - guard index <= categories.count - 2 else { return } - logs.wrappedValue = "" - let categoryEnum = try XCTUnwrap(categoryfromString(categories[index + 1]), - "LogCategory `\(category)` not added to the Category enum.") - - Logger.setLogLevel(.trace, for: categoryEnum) - XCTAssertEqual(Logger.logLevel(for: categoryEnum), .trace) - - RLMTestLog(ObjectiveCSupport.convert(value: categoryEnum), .trace, "Test") - XCTAssertFalse(logs.value.contains("\(LogLevel.trace.logLevel) \(category) Test"), - "Log shouldn't contain message from \(category)") - Logger.setLogLevel(.off, for: categoryEnum) - } - } - - /// Logger should log messages from all child categories - func testShouldLogWhenParentCategory() throws { - Logger.setLogLevel(.trace, for: Category.realm) - XCTAssertEqual(Logger.logLevel(for: Category.realm), .trace) - - for category in Logger.allCategories() { - logs.wrappedValue = "" - RLMTestLog(ObjectiveCSupport.convert(value: categoryfromString(category)!), .trace, "Test") - assertContains(logs.value, "\(category)[Trace]: Test") - } - } - - func testChangeCategoryLevel() throws { - Logger.setLogLevel(.trace, for: Category.realm) - XCTAssertEqual(Logger.logLevel(for: Category.realm), .trace) - - for category in Logger.allCategories() { - let categoryEnum = categoryfromString(category) - XCTAssertEqual(Logger.logLevel(for: categoryEnum!), .trace) - } - - Logger.setLogLevel(.all, for: Category.realm) - XCTAssertEqual(Logger.logLevel(for: Category.realm), .all) - - for category in Logger.allCategories() { - let categoryEnum = categoryfromString(category) - XCTAssertEqual(Logger.logLevel(for: categoryEnum!), .all) - } - } - - func testChangeSubCategoryLevel() throws { - Logger.setLogLevel(.off, for: Category.realm) - XCTAssertEqual(Logger.logLevel(for: Category.Storage.all), .off) - XCTAssertEqual(Logger.logLevel(for: Category.Storage.transaction), .off) - XCTAssertEqual(Logger.logLevel(for: Category.Storage.query), .off) - XCTAssertEqual(Logger.logLevel(for: Category.Storage.object), .off) - XCTAssertEqual(Logger.logLevel(for: Category.Storage.notification), .off) - - Logger.setLogLevel(.info, for: Category.Storage.all) - XCTAssertEqual(Logger.logLevel(for: Category.Storage.all), .info) - XCTAssertEqual(Logger.logLevel(for: Category.Storage.transaction), .info) - XCTAssertEqual(Logger.logLevel(for: Category.Storage.query), .info) - XCTAssertEqual(Logger.logLevel(for: Category.Storage.object), .info) - XCTAssertEqual(Logger.logLevel(for: Category.Storage.notification), .info) - - XCTAssertEqual(Logger.logLevel(for: Category.realm), .off) - XCTAssertEqual(Logger.logLevel(for: Category.sdk), .off) - XCTAssertEqual(Logger.logLevel(for: Category.app), .off) - XCTAssertEqual(Logger.logLevel(for: Category.Sync.all), .off) - } - - func testCallbackFilteringForCatgories() throws { - Logger.setLogLevel(.off, for: Category.realm) - Logger.setLogLevel(.info, for: Category.Storage.all) - - RLMTestLog(.storage, .info, "Storage test entry") - assertContains(logs.value, "Storage test entry") - logs.wrappedValue = "" - - RLMTestLog(.storageTransaction, .info, "Transaction test entry") - assertContains(logs.value, "Transaction test entry") - logs.wrappedValue = "" - - RLMTestLog(.realm, .info, "REALM test entry") - XCTAssertFalse(logs.value.contains("REALM test entry")) - } - func categoryfromString(_ string: String) -> LogCategory? { if let category = Category(rawValue: string) { return category