diff --git a/CHANGELOG.md b/CHANGELOG.md index dfe4aa09a8..58c4f21c8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,19 @@ x.y.z Release notes (yyyy-MM-dd) ============================================================= ### Enhancements -* None. +* Added support for filtering logs by category. Users wil have more fine grained control over + the log level for each category as well. + ```swift + Logger.setLogLevel(.info, category: Category.Storage.transactions) + ``` ### Fixed * ([#????](https://github.com/realm/realm-swift/issues/????), since v?.?.?) * None. - +### Deprecations +* `RLMLogger.level`/`Logger.level` has been deprecated in favor of using `RLMLogger.setLevel:forCategory:`/`Logger.setLevel(:category:)` and `RLMLogger.getLevelForCategory:`/`Logger.getLevel(for:)`. +* It is not recommended to initialize a `RLMLogger/Logger` with a level anymore. ### Compatibility * Realm Studio: 15.0.0 or later. diff --git a/Realm.xcodeproj/project.pbxproj b/Realm.xcodeproj/project.pbxproj index 82b6715fd5..ca9405618a 100644 --- a/Realm.xcodeproj/project.pbxproj +++ b/Realm.xcodeproj/project.pbxproj @@ -360,6 +360,7 @@ AC81360F287F21350029F15E /* AsymmetricObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC81360E287F21350029F15E /* AsymmetricObject.swift */; }; AC813612287F21700029F15E /* RLMAsymmetricObject.h in Headers */ = {isa = PBXBuildFile; fileRef = AC813610287F21700029F15E /* RLMAsymmetricObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; AC813613287F21700029F15E /* RLMAsymmetricObject.mm in Sources */ = {isa = PBXBuildFile; fileRef = AC813611287F21700029F15E /* RLMAsymmetricObject.mm */; }; + AC848BEC2BFFA4AF0026A2A6 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC848BEB2BFFA4AF0026A2A6 /* Logger.swift */; }; AC8846762686573B00DF4A65 /* SwiftUISyncTestHostApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC8846752686573B00DF4A65 /* SwiftUISyncTestHostApp.swift */; }; AC8846782686573B00DF4A65 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC8846772686573B00DF4A65 /* ContentView.swift */; }; AC8846B72687BC4100DF4A65 /* SwiftUIServerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC8846B62687BC4100DF4A65 /* SwiftUIServerTests.swift */; }; @@ -912,6 +913,7 @@ AC81360E287F21350029F15E /* AsymmetricObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AsymmetricObject.swift; sourceTree = ""; }; AC813610287F21700029F15E /* RLMAsymmetricObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RLMAsymmetricObject.h; sourceTree = ""; }; AC813611287F21700029F15E /* RLMAsymmetricObject.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RLMAsymmetricObject.mm; sourceTree = ""; }; + AC848BEB2BFFA4AF0026A2A6 /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; AC8846732686573B00DF4A65 /* SwiftUISyncTestHost.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftUISyncTestHost.app; sourceTree = BUILT_PRODUCTS_DIR; }; AC8846752686573B00DF4A65 /* SwiftUISyncTestHostApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUISyncTestHostApp.swift; sourceTree = ""; }; AC8846772686573B00DF4A65 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -1362,6 +1364,7 @@ AC7825B82ACD90BE007ABA4B /* Geospatial.swift */, 5D1534B71CCFF545008976D7 /* LinkingObjects.swift */, 5D660FE41BE98D670021E04F /* List.swift */, + AC848BEB2BFFA4AF0026A2A6 /* Logger.swift */, 0C3BD4D225C1C5AB007CFDD3 /* Map.swift */, 5D660FE51BE98D670021E04F /* Migration.swift */, CF76F80124816B3800890DD2 /* MongoClient.swift */, @@ -2558,6 +2561,7 @@ 3F857A482769291800F9B9B1 /* KeyPathStrings.swift in Sources */, 5D1534B81CCFF545008976D7 /* LinkingObjects.swift in Sources */, 5D660FF21BE98D670021E04F /* List.swift in Sources */, + AC848BEC2BFFA4AF0026A2A6 /* Logger.swift in Sources */, 0C3BD4D325C1C5AB007CFDD3 /* Map.swift in Sources */, 5D660FF31BE98D670021E04F /* Migration.swift in Sources */, CF76F80224816B3800890DD2 /* MongoClient.swift in Sources */, diff --git a/Realm/ObjectServerTests/RLMSyncTestCase.mm b/Realm/ObjectServerTests/RLMSyncTestCase.mm index 2776bad936..edf859587d 100644 --- a/Realm/ObjectServerTests/RLMSyncTestCase.mm +++ b/Realm/ObjectServerTests/RLMSyncTestCase.mm @@ -613,7 +613,7 @@ - (RLMApp *)appWithId:(NSString *)appId { RLMApp *app = [RLMApp appWithConfiguration:config]; RLMSyncManager *syncManager = app.syncManager; syncManager.userAgent = self.name; - RLMLogger.defaultLogger.level = RLMLogLevelWarn; + [RLMLogger setLevel:RLMLogLevelWarn forCategory:RLMLogCategorySync]; return app; } diff --git a/Realm/RLMLogger.h b/Realm/RLMLogger.h index 96ab919671..35bb099f44 100644 --- a/Realm/RLMLogger.h +++ b/Realm/RLMLogger.h @@ -48,6 +48,61 @@ typedef RLM_CLOSED_ENUM(NSUInteger, RLMLogLevel) { RLMLogLevelAll } NS_SWIFT_NAME(LogLevel); +/** + An enum representing different categories of sync-related logging that can be configured. + Setting the log level for a parent category automatically sets the same level for all child categories. + Category hierarchy: +``` + Realm + ├─► Storage + │ ├─► Transaction + │ ├─► Query + │ ├─► Object + │ └─► Notification + ├─► Sync + │ ├─► Client + │ │ ├─► Session + │ │ ├─► Changeset + │ │ ├─► Network + │ │ └─► Reset + │ └─► Server + ├─► App + └─► Sdk +``` +*/ +typedef NS_ENUM(NSUInteger, RLMLogCategory) { + /// Top level log category for Realm, updating this category level would set all other subcategories too. + RLMLogCategoryRealm, + /// Log category for all sdk related logs. + RLMLogCategorySDK, + /// Log category for all app related logs. + RLMLogCategoryApp, + /// Log category for all database related logs. + RLMLogCategoryStorage, + /// Log category for all database transaction related logs. + RLMLogCategoryStorageTransaction, + /// Log category for all database queries related logs. + RLMLogCategoryStorageQuery, + /// Log category for all database object related logs. + RLMLogCategoryStorageObject, + /// Log category for all database notification related logs. + RLMLogCategoryStorageNotification, + /// Log category for all sync related logs. + RLMLogCategorySync, + /// Log category for all sync client related logs. + RLMLogCategorySyncClient, + /// Log category for all sync client session related logs. + RLMLogCategorySyncClientSession, + /// Log category for all sync client changeset related logs. + RLMLogCategorySyncClientChangeset, + /// Log category for all sync client network related logs. + RLMLogCategorySyncClientNetwork, + /// Log category for all sync client reset related logs. + RLMLogCategorySyncClientReset, + /// Log category for all sync server related logs. + RLMLogCategorySyncServer +}; + /// A log callback function which can be set on RLMLogger. /// /// The log function may be called from multiple threads simultaneously, and is @@ -55,26 +110,33 @@ typedef RLM_CLOSED_ENUM(NSUInteger, RLMLogLevel) { RLM_SWIFT_SENDABLE // invoked on a background thread typedef void (^RLMLogFunction)(RLMLogLevel level, NSString *message); +/// A log callback function which can be set on RLMLogger. +/// +/// 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; /** - `RLMLogger` is used for creating your own custom logging logic. + 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`. - RLMLogger.defaultLogger = [[RLMLogger alloc] initWithLevel:RLMLogLevelDebug - logFunction:^(RLMLogLevel level, NSString * message) { - NSLog(@"Realm Log - %lu, %@", (unsigned long)level, message); + RLMLogger.defaultLogger = [[RLMLogger alloc] initWithLogFunction:^(RLMLogLevel level, NSString *category, NSString *message) { + NSLog(@"Realm Log - %lu, %@, %@", (unsigned long)level, category, message); }]; - @note By default default log threshold level is `RLMLogLevelInfo`, and logging strings are output to Apple System Logger. + @note The default log threshold level is `RLMLogLevelInfo` for the log category `RLMLogCategoryRealm`, + and logging strings are output to Apple System Logger. */ @interface RLMLogger : NSObject /** Gets the logging threshold level used by the logger. */ -@property (nonatomic) RLMLogLevel level; +@property (nonatomic) RLMLogLevel level +__attribute__((deprecated("Use `setLevel(level:category)` or `setLevel:category` instead."))); /// :nodoc: - (instancetype)init NS_UNAVAILABLE; @@ -84,16 +146,52 @@ typedef void (^RLMLogFunction)(RLMLogLevel level, NSString *message); @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. + + @note This will set the log level for the log category `RLMLogCategoryRealm`. +*/ +- (instancetype)initWithLevel:(RLMLogLevel)level logFunction:(RLMLogFunction)logFunction +__attribute__((deprecated("Use `initWithLogFunction:` instead."))); + +/** + 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. */ -- (instancetype)initWithLevel:(RLMLogLevel)level logFunction:(RLMLogFunction)logFunction; +- (instancetype)initWithLogFunction:(RLMLogCategoryFunction)logFunction; #pragma mark RLMLogger Default Logger API /** - The current default logger. When setting a logger as default, this logger will be used whenever information must be logged. + 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. */ @property (class) RLMLogger *defaultLogger NS_SWIFT_NAME(shared); +/** + Log a message to the supplied level. + + @param logLevel The log level for the message. + @param category The log category for the message. + @param message The message to log. + */ +- (void)logWithLevel:(RLMLogLevel)logLevel category:(RLMLogCategory)category message:(NSString *)message; + +/** + Sets the gobal log level for a given category. + + @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. +*/ ++ (void)setLevel:(RLMLogLevel)level forCategory:(RLMLogCategory)category NS_REFINED_FOR_SWIFT; + +/** + Gets the global log level for the specified category. + + @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; + @end RLM_HEADER_AUDIT_END(nullability) diff --git a/Realm/RLMLogger.mm b/Realm/RLMLogger.mm index 7cb27e6a1b..dc58ffbd5a 100644 --- a/Realm/RLMLogger.mm +++ b/Realm/RLMLogger.mm @@ -22,11 +22,12 @@ #import -typedef void (^RLMLoggerFunction)(RLMLogLevel level, NSString *message); +typedef void (^RLMLoggerFunction)(RLMLogLevel level, RLMLogCategory category, NSString *message); using namespace realm; using Logger = realm::util::Logger; using Level = Logger::Level; +using LogCategory = realm::util::LogCategory; namespace { static Level levelForLogLevel(RLMLogLevel logLevel) { @@ -61,7 +62,7 @@ static RLMLogLevel logLevelForLevel(Level logLevel) { static NSString* levelPrefix(Level logLevel) { switch (logLevel) { - case Level::off: + case Level::off: return @""; case Level::all: return @""; case Level::trace: return @"Trace"; case Level::debug: return @"Debug"; @@ -74,19 +75,64 @@ static RLMLogLevel logLevelForLevel(Level logLevel) { 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 RLMLogCategoryStorageNotification: return LogCategory::notification; + 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; + }; + REALM_UNREACHABLE(); +} + +static RLMLogCategory logCategoryForCategoryName(std::string category) { + NSDictionary *categories = @{ + @"Realm": @(RLMLogCategoryRealm), + @"Realm.SDK": @(RLMLogCategorySDK), + @"Realm.App": @(RLMLogCategoryApp), + @"Realm.Storage": @(RLMLogCategoryStorage), + @"Realm.Storage.Transaction": @(RLMLogCategoryStorageTransaction), + @"Realm.Storage.Query": @(RLMLogCategoryStorageQuery), + @"Realm.Storage.Object": @(RLMLogCategoryStorageObject), + @"Realm.Storage.Notification": @(RLMLogCategoryStorageNotification), + @"Realm.Sync": @(RLMLogCategorySync), + @"Realm.Sync.Client": @(RLMLogCategorySyncClient), + @"Realm.Sync.Client.Session": @(RLMLogCategorySyncClientSession), + @"Realm.Sync.Client.Changeset": @(RLMLogCategorySyncClientChangeset), + @"Realm.Sync.Client.Network": @(RLMLogCategorySyncClientNetwork), + @"Realm.Sync.Client.Reset": @(RLMLogCategorySyncClientReset), + @"Realm.Sync.Server": @(RLMLogCategorySyncServer) + }; + if (NSNumber *logCategory = [categories objectForKey:RLMStringDataToNSString(category)]) { + return RLMLogCategory([logCategory intValue]); + } + REALM_UNREACHABLE(); +} + struct CocoaLogger : public Logger { - void do_log(const realm::util::LogCategory&, Level level, const std::string& message) override { - NSLog(@"%@: %@", levelPrefix(level), RLMStringDataToNSString(message)); + void do_log(const LogCategory& category, Level level, const std::string& message) override { + NSLog(@"%@:%@ %@", levelPrefix(level), RLMStringDataToNSString(category.get_name()), RLMStringDataToNSString(message)); } }; class CustomLogger : public Logger { public: RLMLoggerFunction function; - void do_log(const realm::util::LogCategory&, Level level, const std::string& message) override { + void do_log(const LogCategory& category, Level level, const std::string& message) override { @autoreleasepool { if (function) { - function(logLevelForLevel(level), RLMStringDataToNSString(message)); + function(logLevelForLevel(level), logCategoryForCategoryName(category.get_name()), RLMStringDataToNSString(message)); } } } @@ -109,7 +155,7 @@ - (void)setLevel:(RLMLogLevel)level { + (void)initialize { auto defaultLogger = std::make_shared(); - defaultLogger->set_level_threshold(Level::info); + defaultLogger->set_level_threshold(LogCategory::realm, Level::info); Logger::set_default_logger(defaultLogger); } @@ -120,11 +166,27 @@ - (instancetype)initWithLogger:(std::shared_ptr)logger { return self; } -- (instancetype)initWithLevel:(RLMLogLevel)level logFunction:(RLMLogFunction)logFunction { +- (instancetype)initWithLevel:(RLMLogLevel)level + logFunction:(RLMLogFunction)logFunction { if (self = [super init]) { auto logger = std::make_shared(); logger->set_level_threshold(levelForLogLevel(level)); - logger->function = logFunction; + auto block = [logFunction](RLMLogLevel level, RLMLogCategory, NSString *message) { + logFunction(level, message); + }; + logger->function = block; + self->_logger = logger; + } + return self; +} + +- (instancetype)initWithLogFunction:(RLMLogCategoryFunction)logFunction { + if (self = [super init]) { + auto logger = std::make_shared(); + auto block = [logFunction](RLMLogLevel level, RLMLogCategory category, NSString *message) { + logFunction(level, category, message); + }; + logger->function = block; self->_logger = logger; } return self; @@ -140,13 +202,48 @@ - (void)logWithLevel:(RLMLogLevel)logLevel message:(NSString *)message, ... { } } -- (void)logLevel:(RLMLogLevel)logLevel message:(NSString *)message { +- (void)logWithLevel:(RLMLogLevel)logLevel category:(RLMLogCategory)category message:(NSString *)message { auto level = levelForLogLevel(logLevel); - if (_logger->would_log(level)) { - _logger->log(level, "%1", message.UTF8String); + LogCategory& cat = categoryForLogCategory(category); + if (_logger->would_log(cat, level)) { + _logger->log(cat, levelForLogLevel(logLevel), message.UTF8String); } } +- (void)logWithLevel:(RLMLogLevel)logLevel categoryName:(NSString *)categoryName message:(NSString *)message { + auto level = levelForLogLevel(logLevel); + LogCategory& lcat = LogCategory::get_category(categoryName.UTF8String); + if (_logger->would_log(lcat, level)) { + _logger->log(lcat, levelForLogLevel(logLevel), message.UTF8String); + } +} + ++ (void)setLevel:(RLMLogLevel)level forCategory:(RLMLogCategory)category { + auto defaultLogger = Logger::get_default_logger(); + defaultLogger->set_level_threshold(categoryForLogCategory(category).get_name(), levelForLogLevel(level)); +} + ++ (RLMLogLevel)levelForCategory:(RLMLogCategory)category { + auto defaultLogger = Logger::get_default_logger(); + return logLevelForLevel(defaultLogger->get_level_threshold(categoryForLogCategory(category).get_name())); +} + +#pragma mark Testing + ++ (NSArray *)allCategories { + NSMutableArray *a = [NSMutableArray new]; + auto categories = LogCategory::get_category_names(); + for (const auto& category : categories) { + NSString *categoryName = RLMStringDataToNSString(category); + [a addObject:categoryName]; + } + return a; +} + ++ (RLMLogCategory)categoryFromString:(NSString *)string { + return logCategoryForCategoryName(string.UTF8String); +} + #pragma mark Global Logger Setter + (instancetype)defaultLogger { diff --git a/Realm/RLMLogger_Private.h b/Realm/RLMLogger_Private.h index 08f8c591eb..b831937d37 100644 --- a/Realm/RLMLogger_Private.h +++ b/Realm/RLMLogger_Private.h @@ -30,7 +30,27 @@ RLM_HEADER_AUDIT_BEGIN(nullability) @param message The message to log. */ - (void)logWithLevel:(RLMLogLevel)logLevel message:(NSString *)message, ... NS_SWIFT_UNAVAILABLE(""); -- (void)logLevel:(RLMLogLevel)logLevel message:(NSString *)message; + +/** + Log a message to the supplied level. + + @param logLevel The log level for the message. + @param categoryName The log category name for the message. + @param message The message to log. + */ +- (void)logWithLevel:(RLMLogLevel)logLevel categoryName:(NSString *)categoryName message:(NSString *)message; + +#pragma mark Testing + +/** +Gets all the categories from Core. This is to be used for testing purposes only. + */ ++ (NSArray *)allCategories; + +/** +Returns a `RLMLogCategory` from a string. + */ ++ (RLMLogCategory)categoryFromString:(NSString *)string; @end RLM_HEADER_AUDIT_END(nullability) diff --git a/Realm/RLMSyncManager.mm b/Realm/RLMSyncManager.mm index 6a76b62b87..b5acbfa202 100644 --- a/Realm/RLMSyncManager.mm +++ b/Realm/RLMSyncManager.mm @@ -38,6 +38,7 @@ // NEXT-MAJOR: All the code associated to the logger from sync manager should be removed. using Level = realm::util::Logger::Level; +using LogCategory = realm::util::LogCategory; namespace { Level levelForSyncLogLevel(RLMSyncLogLevel logLevel) { @@ -73,14 +74,14 @@ RLMSyncLogLevel logLevelForLevel(Level logLevel) { #pragma mark - Loggers struct CocoaSyncLogger : public realm::util::Logger { - void do_log(const realm::util::LogCategory&, Level, const std::string& message) override { - NSLog(@"Sync: %@", RLMStringDataToNSString(message)); + void do_log(const realm::util::LogCategory& category, Level, const std::string& message) override { + NSLog(@"%s: %s", category.get_name().c_str(), message.c_str()); } }; static std::unique_ptr defaultSyncLogger(realm::util::Logger::Level level) { auto logger = std::make_unique(); - logger->set_level_threshold(level); + logger->set_level_threshold(LogCategory::sync, level); return std::move(logger); } @@ -154,7 +155,7 @@ - (void)setLogger:(RLMSyncLogFunction)logFn { _syncManager->set_logger_factory([logFn](realm::util::Logger::Level level) { auto logger = std::make_unique(); logger->logFn = logFn; - logger->set_level_threshold(level); + logger->set_level_threshold(LogCategory::sync, level); return logger; }); } diff --git a/Realm/Tests/RealmTests.mm b/Realm/Tests/RealmTests.mm index 3dc3d2090d..af03653b79 100644 --- a/Realm/Tests/RealmTests.mm +++ b/Realm/Tests/RealmTests.mm @@ -1475,7 +1475,7 @@ - (void)testAsyncTransactionShouldWrite { [asyncComplete fulfill]; XCTAssertNil(error); }]; - + [self waitForExpectationsWithTimeout:1.0 handler:nil]; } @@ -1774,7 +1774,7 @@ - (void)testAsyncBeginTransactionInAsyncTransaction { [realm beginAsyncWriteTransaction:^{ [StringObject createInRealm:realm withValue:@[@"string"]]; - + [realm commitAsyncWriteTransaction:^(NSError *error) { XCTAssertEqual(0U, [StringObject allObjects].count); [transaction1 fulfill]; @@ -1801,7 +1801,7 @@ - (void)testAsyncTransactionFromSyncTransaction { [realm beginWriteTransaction]; [StringObject createInRealm:realm withValue:@[@"string"]]; - + [realm beginAsyncWriteTransaction:^{ [StringObject createInRealm:realm withValue:@[@"string"]]; @@ -1823,7 +1823,7 @@ - (void)testAsyncNestedWrites { [self dispatchAsync:^{ RLMRealm *realm = [RLMRealm defaultRealmForQueue:self.bgQueue]; - + [realm beginAsyncWriteTransaction:^{ [StringObject createInRealm:realm withValue:@[@"string 1"]]; @@ -1941,7 +1941,7 @@ - (void)testAsyncWriteOnQueueConfinedRealm { }]; }]; }); - + [self waitForExpectationsWithTimeout:2.0 handler:nil]; XCTAssertEqual(1U, [StringObject allObjectsInRealm:[RLMRealm defaultRealm]].count); } @@ -1977,7 +1977,7 @@ - (void)testAsyncCancelWtongTransaction { }]; [realm cancelAsyncTransaction:transId+1]; - + [self waitForExpectationsWithTimeout:2.0 handler:nil]; XCTAssertEqual(1U, [StringObject allObjectsInRealm:[RLMRealm defaultRealm]].count); } @@ -2008,7 +2008,7 @@ - (void)testAsyncIsInTransaction { XCTAssertFalse(realm.isPerformingAsynchronousWriteOperations); XCTAssertFalse(realm.inWriteTransaction); - + [realm beginWriteTransaction]; XCTAssertFalse(realm.isPerformingAsynchronousWriteOperations); XCTAssertTrue(realm.inWriteTransaction); @@ -2589,7 +2589,7 @@ - (void)testThaw { - (void)testThawDifferentThread { RLMRealm *frozenRealm = [[RLMRealm defaultRealm] freeze]; XCTAssertTrue(frozenRealm.frozen); - + // Thaw on a thread which already has a Realm should use existing reference. [self dispatchAsyncAndWait:^{ RLMRealm *realm = [RLMRealm defaultRealm]; @@ -2597,7 +2597,7 @@ - (void)testThawDifferentThread { XCTAssertFalse(thawed.frozen); XCTAssertEqual(thawed, realm); }]; - + // Thaw on thread without existing refernce. [self dispatchAsyncAndWait:^{ RLMRealm *thawed = [frozenRealm thaw]; @@ -2960,45 +2960,48 @@ - (void)tearDown { } - (void)testSetDefaultLogLevel { __block NSMutableString *logs = [[NSMutableString alloc] init]; - RLMLogger *logger = [[RLMLogger alloc] initWithLevel:RLMLogLevelAll logFunction:^(RLMLogLevel level, NSString *message) { - [logs appendFormat:@" %@ %lu %@", [NSDate date], level, message]; + 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:RLMLogLevelAll forCategory:category]; @autoreleasepool { [RLMRealm defaultRealm]; } - XCTAssertEqual([RLMLogger defaultLogger].level, RLMLogLevelAll); + XCTAssertEqual([RLMLogger levelForCategory:category], RLMLogLevelAll); XCTAssertTrue([logs containsString:@"5 DB:"]); // Detail XCTAssertTrue([logs containsString:@"7 DB:"]); // Trace [logs setString: @""]; - logger.level = RLMLogLevelDetail; + [RLMLogger setLevel:RLMLogLevelDetail forCategory:category]; @autoreleasepool { [RLMRealm defaultRealm]; } - XCTAssertEqual([RLMLogger defaultLogger].level, RLMLogLevelDetail); + 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]; - RLMLogger *logger = [[RLMLogger alloc] initWithLevel:RLMLogLevelOff - logFunction:^(RLMLogLevel level, NSString *message) { - [logs appendFormat:@" %@ %lu %@", [NSDate date], level, message]; + 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; - XCTAssertEqual(RLMLogger.defaultLogger.level, RLMLogLevelOff); + [RLMLogger setLevel:RLMLogLevelOff forCategory:category]; + XCTAssertEqual([RLMLogger levelForCategory:category], RLMLogLevelOff); @autoreleasepool { [RLMRealm defaultRealm]; } XCTAssertTrue([logs length] == 0); // Test LogLevel Detail - logger.level = RLMLogLevelDetail; + [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 - logger.level = RLMLogLevelAll; + [RLMLogger setLevel:RLMLogLevelAll forCategory:category]; @autoreleasepool { [RLMRealm defaultRealm]; } XCTAssertTrue([logs length] > 0); XCTAssertTrue([logs containsString:@"5 DB:"]); // Detail @@ -3006,12 +3009,11 @@ - (void)testDefaultLogger { [logs setString: @""]; // Init Custom Logger - RLMLogger.defaultLogger = [[RLMLogger alloc] initWithLevel:RLMLogLevelDebug - logFunction:^(RLMLogLevel level, NSString * message) { - [logs appendFormat:@" %@ %lu %@", [NSDate date], level, message]; + RLMLogger.defaultLogger = [[RLMLogger alloc] initWithLogFunction:^(RLMLogLevel level, RLMLogCategory category, NSString * message) { + [logs appendFormat:@" %@ %lu %lu %@", [NSDate date], (unsigned long)category, level, message]; }]; - - XCTAssertEqual(RLMLogger.defaultLogger.level, RLMLogLevelDebug); + [RLMLogger setLevel:RLMLogLevelDebug forCategory:category]; + XCTAssertEqual([RLMLogger levelForCategory:category], RLMLogLevelDebug); @autoreleasepool { [RLMRealm defaultRealm]; } XCTAssertTrue([logs containsString:@"5 DB:"]); // Detail XCTAssertFalse([logs containsString:@"7 DB:"]); // Trace @@ -3019,17 +3021,26 @@ - (void)testDefaultLogger { - (void)testCustomLoggerLogMessage { __block NSMutableString *logs = [[NSMutableString alloc] init]; - RLMLogger *logger = [[RLMLogger alloc] initWithLevel:RLMLogLevelInfo - logFunction:^(RLMLogLevel level, NSString * message) { - [logs appendFormat:@" %@ %lu %@.", [NSDate date], level, message]; + 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:RLMLogLevelDebug forCategory:category]; [logger logWithLevel:RLMLogLevelInfo message:@"%@ IMPORTANT INFO %i", @"TEST:", 0]; [logger logWithLevel:RLMLogLevelTrace message:@"IMPORTANT TRACE"]; XCTAssertTrue([logs containsString:@"TEST: IMPORTANT INFO 0"]); // Detail XCTAssertFalse([logs containsString:@"IMPORTANT TRACE"]); // Trace } + +// 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 RLMLogCategory enum. +- (void)testAllCategoriesWatchDog { + for (id category in [RLMLogger allCategories]) { + XCTAssertNoThrow([RLMLogger categoryFromString:category]); + } +} @end @interface RLMMetricsTests : RLMTestCase @@ -3046,11 +3057,12 @@ - (void)tearDown { - (void)testSyncConnectionMetrics { __block NSMutableString *logs = [[NSMutableString alloc] init]; - RLMLogger *logger = [[RLMLogger alloc] initWithLevel:RLMLogLevelDebug - logFunction:^(RLMLogLevel level, NSString * message) { - [logs appendFormat:@" %@ %lu %@\n", [NSDate date], level, message]; + 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:RLMLogLevelAll forCategory:category]; 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/Realm/Tests/SwiftUISyncTestHost/ContentView.swift b/Realm/Tests/SwiftUISyncTestHost/ContentView.swift index 66a22fae49..e7d22b17fb 100644 --- a/Realm/Tests/SwiftUISyncTestHost/ContentView.swift +++ b/Realm/Tests/SwiftUISyncTestHost/ContentView.swift @@ -198,7 +198,7 @@ class LoginHelper: ObservableObject { private let appConfig = AppConfiguration(baseURL: "http://localhost:9090") func login(email: String, password: String, completion: @escaping (User) -> Void) { - Logger.shared.level = .all + Logger.setLogLevel(.all, for: Category.realm) let app = RealmSwift.App(id: ProcessInfo.processInfo.environment["app_id"]!, configuration: appConfig) app.login(credentials: .emailPassword(email: email, password: password)) .receive(on: DispatchQueue.main) diff --git a/RealmSwift/Logger.swift b/RealmSwift/Logger.swift new file mode 100644 index 0000000000..3bd00b97e7 --- /dev/null +++ b/RealmSwift/Logger.swift @@ -0,0 +1,331 @@ +//////////////////////////////////////////////////////////////////////////// +// +// 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. +// +//////////////////////////////////////////////////////////////////////////// + +import Realm +import Realm.Private + +/** + Global logger class used by all Realm components. + + Set the global log level for a given category. + ```swift + Logger.setLogLevel(.info, for: Category.sdk) + ``` + + Read the global log level for a given category. + ```swift + let level = Logger.logLevel(for: Category.Storage.all) + ``` + + You can define your own custom logger creating an instance of `Logger` and defining the log function which will be + invoked whenever there is a log message. + + ```swift + let logger = Logger(function: { level, category, message in + print("Realm Log - \(category.rawValue)-\(level): \(message)") + }) + ``` + + Set this custom logger as you default logger using `Logger.shared`. This will replace the default logger. + + ```swift + Logger.shared = logger + ``` + + - note: The default log threshold level is `.info`, for the log category `.Category.realm`, + and logging strings are output to Apple System Logger. + - SeeAlso: `LogCategory` +*/ +public typealias Logger = RLMLogger +extension Logger { + /** + Log a message to the supplied level. + + ```swift + let logger = Logger(level: .info, logFunction: { level, message in + print("Realm Log - \(level): \(message)") + }) + logger.log(level: .info, message: "Info DB: Database opened succesfully") + ``` + + - parameter level: The log level for the message. + - parameter category: The log category for the message. + - parameter message: The message to log. + */ + internal func log(level: LogLevel, category: LogCategory = Category.sdk, message: String) { + self.log(with: level, category: ObjectiveCSupport.convert(value: category), message: message) + } + + /** + 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. + + - parameter level: The log level to be set for the logger. + - parameter category: The log category to be set for the logger, by default it will setup the top Category `Category.realm` + + - 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) { + Logger.__setLevel(level, for: ObjectiveCSupport.convert(value: category)) + } + + /** + Gets the current global log level of a log category. + + - parameter category: The target log category. + + - returns: The `LogLevel` for the given category. + - SeeAlso: `LogCategory` + */ + public static func logLevel(for category: LogCategory) -> LogLevel { + Logger.__level(for: ObjectiveCSupport.convert(value: category)) + } +} + +/// Defines a log category for the Realm `Logger`. +public protocol LogCategory: Sendable { + /** + Returns the string represtation of the Log category. + + - returns: A string representing the log category. + - SeeAlso: `LogCategory` + */ + var rawValue: String { get } +} + +/** + Category hierarchy: + ``` + Realm + ├─► Storage + │ ├─► Transaction + │ ├─► Query + │ ├─► Object + │ └─► Notification + ├─► Sync + │ ├─► Client + │ │ ├─► Session + │ │ ├─► Changeset + │ │ ├─► Network + │ │ └─► Reset + │ └─► Server + ├─► App + └─► Sdk + ``` +*/ +public enum Category: String, LogCategory { + /// Top level log category for Realm, updating this category level would set all other subcategories too. + case realm = "Realm" + /// Log category for all sdk related logs. + case sdk = "Realm.SDK" + /// Log category for all app related logs. + case app = "Realm.App" + + /** + Log category for all storage related logs. + + Category hierarchy: + ``` + Storage + ├─► Transaction + ├─► Query + ├─► Object + └─► Notification + ``` + */ + public enum Storage: String, LogCategory { + /// Log category for all database related logs. + case all = "Realm.Storage" + /// Log category for all database transaction related logs. + case transaction = "Realm.Storage.Transaction" + /// Log category for all database queries related logs. + case query = "Realm.Storage.Query" + /// Log category for all database object related logs. + case object = "Realm.Storage.Object" + /// Log category for all database notification related logs. + case notification = "Realm.Storage.Notification" + } + + /** + Log category for all sync related logs. + + Category hierarchy: + ``` + Sync + ├─► Client + │ ├─► Session + │ ├─► Changeset + │ ├─► Network + │ └─► Reset + └─► Server + ``` + */ + public enum Sync: String, LogCategory { + /// Log category for all sync related logs. + case all = "Realm.Sync" + /// Log category for all sync server related logs. + case server = "Realm.Sync.Server" + + /** + Log category for all storage related logs. + + Category hierarchy: + ``` + Client + ├─► Session + ├─► Changeset + ├─► Network + └─► Reset + ``` + */ + public enum Client: String, LogCategory { + /// Log category for all sync client related logs. + case all = "Realm.Sync.Client" + /// Log category for all sync client session related logs. + case session = "Realm.Sync.Client.Session" + /// Log category for all sync client changeset related logs. + case changeset = "Realm.Sync.Client.Changeset" + /// Log category for all sync client network related logs. + case network = "Realm.Sync.Client.Network" + /// Log category for all sync client reset related logs. + case reset = "Realm.Sync.Client.Reset" + } + } +} + +private 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. + static func convert(value: LogCategory) -> RLMLogCategory { + switch value { + case Category.realm: + return RLMLogCategory.realm + case Category.sdk: + return RLMLogCategory.SDK + case Category.app: + return RLMLogCategory.app + case Category.Storage.all: + return RLMLogCategory.storage + case Category.Storage.transaction: + return RLMLogCategory.storageTransaction + case Category.Storage.query: + return RLMLogCategory.storageQuery + case Category.Storage.object: + return RLMLogCategory.storageObject + case Category.Storage.notification: + return RLMLogCategory.storageNotification + case Category.Sync.all: + return RLMLogCategory.sync + case Category.Sync.Client.all: + return RLMLogCategory.syncClient + case Category.Sync.Client.session: + return RLMLogCategory.syncClientSession + case Category.Sync.Client.changeset: + return RLMLogCategory.syncClientChangeset + case Category.Sync.Client.network: + return RLMLogCategory.syncClientNetwork + case Category.Sync.Client.reset: + return RLMLogCategory.syncClientReset + case Category.Sync.server: + return RLMLogCategory.syncServer + default: + fatalError() + } + } + + /// Converts an Objective-C category `RLMLogCategory` to a Swift `LogCategory. + /// - Parameter value: The `RLMLogCategory`. + /// - Returns: Conversion of `value` to its Swift representation. + static func convert(value: RLMLogCategory) -> LogCategory { + switch value { + case RLMLogCategory.realm: + return Category.realm + case RLMLogCategory.SDK: + return Category.sdk + case RLMLogCategory.app: + return Category.app + case RLMLogCategory.storage: + return Category.Storage.all + case RLMLogCategory.storageTransaction: + return Category.Storage.transaction + case RLMLogCategory.storageQuery: + return Category.Storage.query + case RLMLogCategory.storageObject: + return Category.Storage.object + case RLMLogCategory.storageNotification: + return Category.Storage.notification + case RLMLogCategory.sync: + return Category.Sync.all + case RLMLogCategory.syncClient: + return Category.Sync.Client.all + case RLMLogCategory.syncClientSession: + return Category.Sync.Client.session + case RLMLogCategory.syncClientChangeset: + return Category.Sync.Client.changeset + case RLMLogCategory.syncClientNetwork: + return Category.Sync.Client.network + case RLMLogCategory.syncClientReset: + return Category.Sync.Client.reset + case RLMLogCategory.syncServer: + return Category.Sync.server + default: + fatalError() + } + } +} diff --git a/RealmSwift/Realm.swift b/RealmSwift/Realm.swift index 5a5bed9b18..3e7473354c 100644 --- a/RealmSwift/Realm.swift +++ b/RealmSwift/Realm.swift @@ -1631,43 +1631,3 @@ extension Projection: RealmFetchable { return Root.className() } } - -/** - `Logger` is used for creating your own custom logging logic. - - You can define your own logger creating an instance of `Logger` and define the log function which will be - invoked whenever there is a log message. - - ```swift - let logger = Logger(level: .all) { level, message in - print("Realm Log - \(level): \(message)") - } - ``` - - Set this custom logger as you default logger using `Logger.shared`. - - ```swift - Logger.shared = inMemoryLogger - ``` - - - note: By default default log threshold level is `.info`, and logging strings are output to Apple System Logger. -*/ -public typealias Logger = RLMLogger -extension Logger { - /** - Log a message to the supplied level. - - ```swift - let logger = Logger(level: .info, logFunction: { level, message in - print("Realm Log - \(level): \(message)") - }) - logger.log(level: .info, message: "Info DB: Database opened succesfully") - ``` - - - parameter level: The log level for the message. - - parameter message: The message to log. - */ - internal func log(level: LogLevel, message: String) { - self.logLevel(level, message: message) - } -} diff --git a/RealmSwift/Tests/RealmTests.swift b/RealmSwift/Tests/RealmTests.swift index 5539141468..5cbe0745dd 100644 --- a/RealmSwift/Tests/RealmTests.swift +++ b/RealmSwift/Tests/RealmTests.swift @@ -23,6 +23,7 @@ #endif import Foundation import Realm +import Realm.Private import XCTest #if canImport(RealmSwiftTestSupport) @@ -1953,66 +1954,239 @@ class LoggerTests: TestCase, @unchecked Sendable { override func tearDown() { Logger.shared = logger } + func testSetDefaultLogLevel() throws { - var logs: String = "" - let logger = Logger(level: .off) { level, message in - logs += "\(Date.now) \(level.logLevel) \(message)" - } + let logs = Locked("") + let logger = Logger(function: { level, category, message in + logs.withLock({ $0 += "\(Date.now) \(category.rawValue):\(level.logLevel) \(message)" }) + }) Logger.shared = logger + Logger.setLogLevel(.off, for: Category.realm) try autoreleasepool { _ = try Realm() } - XCTAssertTrue(logs.isEmpty) + XCTAssertTrue(logs.value.isEmpty) - logger.level = .all + Logger.setLogLevel(.all, for: Category.realm) try autoreleasepool { _ = try Realm() } // We should be getting logs after changing the log level - XCTAssertEqual(Logger.shared.level, .all) - XCTAssertTrue(logs.contains("Details DB:")) - XCTAssertTrue(logs.contains("Trace DB:")) + XCTAssertEqual(Logger.logLevel(for: Category.realm), .all) + XCTAssertTrue(logs.value.contains("Details DB:")) + XCTAssertTrue(logs.value.contains("Trace DB:")) } - func testDefaultLogger() throws { - var logs: String = "" - let logger = Logger(level: .off) { level, message in - logs += "\(Date.now) \(level.logLevel) \(message)" - } + func testSetDefaultLogger() throws { + let logs = Locked("") + let logger = Logger(function: { level, category, message in + logs.withLock({ $0 += "\(Date.now) \(category.rawValue):\(level.logLevel) \(message)" }) + }) Logger.shared = logger - - XCTAssertEqual(Logger.shared.level, .off) + Logger.setLogLevel(.off, for: Category.realm) + XCTAssertEqual(Logger.logLevel(for: Category.realm), .off) try autoreleasepool { _ = try Realm() } - XCTAssertTrue(logs.isEmpty) + XCTAssertTrue(logs.value.isEmpty) // Info - logger.level = .detail + Logger.setLogLevel(.detail, for: Category.realm) try autoreleasepool { _ = try Realm() } - XCTAssertTrue(!logs.isEmpty) - XCTAssertTrue(logs.contains("Details DB:")) + XCTAssertTrue(!logs.value.isEmpty) + XCTAssertTrue(logs.value.contains("Details DB:")) // Trace - logs = "" - logger.level = .trace + logs.wrappedValue = "" + Logger.setLogLevel(.trace, for: Category.realm) try autoreleasepool { _ = try Realm() } - XCTAssertTrue(!logs.isEmpty) - XCTAssertTrue(logs.contains("Trace DB:")) + XCTAssertTrue(!logs.value.isEmpty) + XCTAssertTrue(logs.value.contains("Trace DB:")) // Detail - logs = "" - logger.level = .detail + logs.wrappedValue = "" + Logger.setLogLevel(.detail, for: Category.realm) try autoreleasepool { _ = try Realm() } - XCTAssertTrue(!logs.isEmpty) - XCTAssertTrue(logs.contains("Details DB:")) - XCTAssertFalse(logs.contains("Trace DB:")) + XCTAssertTrue(!logs.value.isEmpty) + XCTAssertTrue(logs.value.contains("Details DB:")) + XCTAssertFalse(logs.value.contains("Trace DB:")) - logs = "" - Logger.shared = Logger(level: .trace) { level, message in - logs += "\(Date.now) \(level.logLevel) \(message)" - } - XCTAssertEqual(Logger.shared.level, .trace) + logs.wrappedValue = "" + 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() } - XCTAssertTrue(!logs.isEmpty) - XCTAssertTrue(logs.contains("Details DB:")) - XCTAssertTrue(logs.contains("Trace DB:")) + XCTAssertTrue(!logs.value.isEmpty) + XCTAssertTrue(logs.value.contains("Details DB:")) + XCTAssertTrue(logs.value.contains("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) + } + } + + func testLogMessageForCategory() throws { + let logs = Locked("") + let logger = Logger(function: { level, category, message in + logs.withLock({ $0 += "\(level.logLevel) \(category.rawValue) \(message) " }) + }) + Logger.shared = logger + + for category in Logger.allCategories() { + logs.wrappedValue = "" + 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) + logger.log(with: .trace, categoryName: category, message: "Test") + XCTAssertTrue(logs.value.contains("\(LogLevel.trace.logLevel) \(category) Test"), "Log doesn't contain \(category)") + } + } + + /// 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 logs = Locked("") + let logger = Logger(function: { level, category, message in + logs.withLock({ $0 += "\(level.logLevel) \(category.rawValue) \(message) " }) + }) + Logger.shared = logger + + let categories = Logger.allCategories() + for (index, category) in categories.enumerated() { + guard index <= categories.count-2 else { return } + logs.wrappedValue = "" + let categoryEnum = categoryfromString(categories[index+1]) + XCTAssertNotNil(categoryEnum, "LogCategory `\(category)` not added to the Category enum.") + + Logger.setLogLevel(.trace, for: categoryEnum!) + XCTAssertEqual(Logger.logLevel(for: categoryEnum!), .trace) + + logger.log(with: .trace, categoryName: category, message: "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 { + let logs = Locked("") + let logger = Logger(function: { level, category, message in + logs.withLock({ $0 += "\(level.logLevel) \(category.rawValue) \(message) " }) + }) + Logger.shared = logger + Logger.setLogLevel(.trace, for: Category.realm) + XCTAssertEqual(Logger.logLevel(for: Category.realm), .trace) + + for category in Logger.allCategories() { + logs.wrappedValue = "" + logger.log(with: .trace, categoryName: category, message: "Test") + XCTAssertTrue(logs.value.contains("\(LogLevel.trace.logLevel) \(category) Test"), "Log doesn't contain \( Category.realm.rawValue)") + } + } + + func testChangeCategoryLevel() throws { + let logs = Locked("") + let logger = Logger(function: { level, category, message in + logs.withLock({ $0 += "\(level.logLevel) \(category.rawValue) \(message) " }) + }) + Logger.shared = logger + + 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 { + let logs = Locked("") + let logger = Logger(function: { level, _, message in + logs.withLock({ $0 += "\(Date.now) \(level.logLevel) \(message)" }) + }) + + Logger.shared = logger + + Logger.setLogLevel(.off, for: Category.realm) + Logger.setLogLevel(.info, for: Category.Storage.all) + + logger.log(with: .info, categoryName: Category.Storage.all.rawValue, message: "Storage test entry") + XCTAssertTrue(logs.value.contains("Storage test entry")) + logs.wrappedValue = "" + + logger.log(with: .info, categoryName: Category.Storage.transaction.rawValue, message: "Transaction test entry") + XCTAssertTrue(logs.value.contains("Transaction test entry")) + logs.wrappedValue = "" + + logger.log(with: .info, categoryName: Category.realm.rawValue, message: "REALM test entry") + XCTAssertFalse(logs.value.contains("REALM test entry")) + } + + func categoryfromString(_ string: String) -> LogCategory? { + if let category = Category(rawValue: string) { + return category + } else if let storage = Category.Storage(rawValue: string) { + return storage + } else if let sync = Category.Sync(rawValue: string) { + return sync + } else if let client = Category.Sync.Client(rawValue: string) { + return client + } else { + return nil + } } }