| #import "FMDBLogger.h" |
| #import "FMDatabase.h" |
| |
| |
| @interface FMDBLogger () |
| - (void)validateLogDirectory; |
| - (void)openDatabase; |
| @end |
| |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| #pragma mark - |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| |
| @interface FMDBLogEntry : NSObject { |
| @public |
| NSNumber * context; |
| NSNumber * level; |
| NSString * message; |
| NSDate * timestamp; |
| } |
| |
| - (id)initWithLogMessage:(DDLogMessage *)logMessage; |
| |
| @end |
| |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| #pragma mark - |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| |
| @implementation FMDBLogEntry |
| |
| - (id)initWithLogMessage:(DDLogMessage *)logMessage |
| { |
| if ((self = [super init])) |
| { |
| context = @(logMessage->_context); |
| level = @(logMessage->_flag); |
| message = logMessage->_message; |
| timestamp = logMessage->_timestamp; |
| } |
| return self; |
| } |
| |
| |
| @end |
| |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| #pragma mark - |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| |
| @implementation FMDBLogger |
| |
| - (id)initWithLogDirectory:(NSString *)aLogDirectory |
| { |
| if ((self = [super init])) |
| { |
| logDirectory = [aLogDirectory copy]; |
| |
| pendingLogEntries = [[NSMutableArray alloc] initWithCapacity:_saveThreshold]; |
| |
| [self validateLogDirectory]; |
| [self openDatabase]; |
| } |
| |
| return self; |
| } |
| |
| |
| - (void)validateLogDirectory |
| { |
| // Validate log directory exists or create the directory. |
| |
| BOOL isDirectory; |
| if ([[NSFileManager defaultManager] fileExistsAtPath:logDirectory isDirectory:&isDirectory]) |
| { |
| if (!isDirectory) |
| { |
| NSLog(@"%@: %@ - logDirectory(%@) is a file!", [self class], THIS_METHOD, logDirectory); |
| |
| logDirectory = nil; |
| } |
| } |
| else |
| { |
| NSError *error = nil; |
| |
| BOOL result = [[NSFileManager defaultManager] createDirectoryAtPath:logDirectory |
| withIntermediateDirectories:YES |
| attributes:nil |
| error:&error]; |
| if (!result) |
| { |
| NSLog(@"%@: %@ - Unable to create logDirectory(%@) due to error: %@", |
| [self class], THIS_METHOD, logDirectory, error); |
| |
| logDirectory = nil; |
| } |
| } |
| } |
| |
| - (void)openDatabase |
| { |
| if (logDirectory == nil) |
| { |
| return; |
| } |
| |
| NSString *path = [logDirectory stringByAppendingPathComponent:@"log.sqlite"]; |
| |
| database = [[FMDatabase alloc] initWithPath:path]; |
| |
| if (![database open]) |
| { |
| NSLog(@"%@: Failed opening database!", [self class]); |
| |
| database = nil; |
| |
| return; |
| } |
| |
| NSString *cmd1 = @"CREATE TABLE IF NOT EXISTS logs (context integer, " |
| "level integer, " |
| "message text, " |
| "timestamp double)"; |
| [database executeUpdate:cmd1]; |
| if ([database hadError]) |
| { |
| NSLog(@"%@: Error creating table: code(%d): %@", |
| [self class], [database lastErrorCode], [database lastErrorMessage]); |
| |
| database = nil; |
| } |
| |
| NSString *cmd2 = @"CREATE INDEX IF NOT EXISTS timestamp ON logs (timestamp)"; |
| |
| [database executeUpdate:cmd2]; |
| if ([database hadError]) |
| { |
| NSLog(@"%@: Error creating index: code(%d): %@", |
| [self class], [database lastErrorCode], [database lastErrorMessage]); |
| |
| database = nil; |
| } |
| |
| [database setShouldCacheStatements:YES]; |
| } |
| |
| #pragma mark AbstractDatabaseLogger Overrides |
| |
| - (BOOL)db_log:(DDLogMessage *)logMessage |
| { |
| // You may be wondering, how come we don't just do the insert here and be done with it? |
| // Is the buffering really needed? |
| // |
| // From the SQLite FAQ: |
| // |
| // (19) INSERT is really slow - I can only do few dozen INSERTs per second |
| // |
| // Actually, SQLite will easily do 50,000 or more INSERT statements per second on an average desktop computer. |
| // But it will only do a few dozen transactions per second. Transaction speed is limited by the rotational |
| // speed of your disk drive. A transaction normally requires two complete rotations of the disk platter, which |
| // on a 7200RPM disk drive limits you to about 60 transactions per second. |
| // |
| // Transaction speed is limited by disk drive speed because (by default) SQLite actually waits until the data |
| // really is safely stored on the disk surface before the transaction is complete. That way, if you suddenly |
| // lose power or if your OS crashes, your data is still safe. For details, read about atomic commit in SQLite. |
| // |
| // By default, each INSERT statement is its own transaction. But if you surround multiple INSERT statements |
| // with BEGIN...COMMIT then all the inserts are grouped into a single transaction. The time needed to commit |
| // the transaction is amortized over all the enclosed insert statements and so the time per insert statement |
| // is greatly reduced. |
| |
| FMDBLogEntry *logEntry = [[FMDBLogEntry alloc] initWithLogMessage:logMessage]; |
| |
| [pendingLogEntries addObject:logEntry]; |
| |
| // Return YES if an item was added to the buffer. |
| // Return NO if the logMessage was ignored. |
| |
| return YES; |
| } |
| |
| - (void)db_save |
| { |
| if ([pendingLogEntries count] == 0) |
| { |
| // Nothing to save. |
| // The superclass won't likely call us if this is the case, but we're being cautious. |
| return; |
| } |
| |
| BOOL saveOnlyTransaction = ![database inTransaction]; |
| |
| if (saveOnlyTransaction) |
| { |
| [database beginTransaction]; |
| } |
| |
| NSString *cmd = @"INSERT INTO logs (context, level, message, timestamp) VALUES (?, ?, ?, ?)"; |
| |
| for (FMDBLogEntry *logEntry in pendingLogEntries) |
| { |
| [database executeUpdate:cmd, logEntry->context, |
| logEntry->level, |
| logEntry->message, |
| logEntry->timestamp]; |
| } |
| |
| [pendingLogEntries removeAllObjects]; |
| |
| if (saveOnlyTransaction) |
| { |
| [database commit]; |
| |
| if ([database hadError]) |
| { |
| NSLog(@"%@: Error inserting log entries: code(%d): %@", |
| [self class], [database lastErrorCode], [database lastErrorMessage]); |
| } |
| } |
| } |
| |
| - (void)db_delete |
| { |
| if (_maxAge <= 0.0) |
| { |
| // Deleting old log entries is disabled. |
| // The superclass won't likely call us if this is the case, but we're being cautious. |
| return; |
| } |
| |
| BOOL deleteOnlyTransaction = ![database inTransaction]; |
| |
| NSDate *maxDate = [NSDate dateWithTimeIntervalSinceNow:(-1.0 * _maxAge)]; |
| |
| [database executeUpdate:@"DELETE FROM logs WHERE timestamp < ?", maxDate]; |
| |
| if (deleteOnlyTransaction) |
| { |
| if ([database hadError]) |
| { |
| NSLog(@"%@: Error deleting log entries: code(%d): %@", |
| [self class], [database lastErrorCode], [database lastErrorMessage]); |
| } |
| } |
| } |
| |
| - (void)db_saveAndDelete |
| { |
| [database beginTransaction]; |
| |
| [self db_delete]; |
| [self db_save]; |
| |
| [database commit]; |
| |
| if ([database hadError]) |
| { |
| NSLog(@"%@: Error: code(%d): %@", |
| [self class], [database lastErrorCode], [database lastErrorMessage]); |
| } |
| } |
| |
| @end |