// // GTMLogger.m // // Copyright 2007-2008 Google 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 "GTMLogger.h" #import #import #import #import #if !defined(__clang__) && (__GNUC__*10+__GNUC_MINOR__ >= 42) // Some versions of GCC (4.2 and below AFAIK) aren't great about supporting // -Wmissing-format-attribute // when the function is anything more complex than foo(NSString *fmt, ...). // You see the error inside the function when you turn ... into va_args and // attempt to call another function (like vsprintf for example). // So we just shut off the warning for this file. We reenable it at the end. #pragma GCC diagnostic ignored "-Wmissing-format-attribute" #endif // !__clang__ // Reference to the shared GTMLogger instance. This is not a singleton, it's // just an easy reference to one shared instance. static GTMLogger *gSharedLogger = nil; @implementation GTMLogger // Returns a pointer to the shared logger instance. If none exists, a standard // logger is created and returned. + (id)sharedLogger { @synchronized(self) { if (gSharedLogger == nil) { gSharedLogger = [[self standardLogger] retain]; } } return [[gSharedLogger retain] autorelease]; } + (void)setSharedLogger:(GTMLogger *)logger { @synchronized(self) { [gSharedLogger autorelease]; gSharedLogger = [logger retain]; } } + (id)standardLogger { // Don't trust NSFileHandle not to throw @try { id writer = [NSFileHandle fileHandleWithStandardOutput]; id fr = [[[GTMLogStandardFormatter alloc] init] autorelease]; id filter = [[[GTMLogLevelFilter alloc] init] autorelease]; return [[[self alloc] initWithWriter:writer formatter:fr filter:filter] autorelease]; } @catch (id e) { // Ignored } return nil; } + (id)standardLoggerWithStderr { // Don't trust NSFileHandle not to throw @try { id me = [self standardLogger]; [me setWriter:[NSFileHandle fileHandleWithStandardError]]; return me; } @catch (id e) { // Ignored } return nil; } + (id)standardLoggerWithStdoutAndStderr { // We're going to take advantage of the GTMLogger to GTMLogWriter adaptor // and create a composite logger that an outer "standard" logger can use // as a writer. Our inner loggers should apply no formatting since the main // logger does that and we want the caller to be able to change formatters // or add writers without knowing the inner structure of our composite. // Don't trust NSFileHandle not to throw @try { GTMLogBasicFormatter *formatter = [[[GTMLogBasicFormatter alloc] init] autorelease]; GTMLogger *stdoutLogger = [self loggerWithWriter:[NSFileHandle fileHandleWithStandardOutput] formatter:formatter filter:[[[GTMLogMaximumLevelFilter alloc] initWithMaximumLevel:kGTMLoggerLevelInfo] autorelease]]; GTMLogger *stderrLogger = [self loggerWithWriter:[NSFileHandle fileHandleWithStandardError] formatter:formatter filter:[[[GTMLogMininumLevelFilter alloc] initWithMinimumLevel:kGTMLoggerLevelError] autorelease]]; GTMLogger *compositeWriter = [self loggerWithWriter:[NSArray arrayWithObjects: stdoutLogger, stderrLogger, nil] formatter:formatter filter:[[[GTMLogNoFilter alloc] init] autorelease]]; GTMLogger *outerLogger = [self standardLogger]; [outerLogger setWriter:compositeWriter]; return outerLogger; } @catch (id e) { // Ignored } return nil; } + (id)standardLoggerWithPath:(NSString *)path { @try { NSFileHandle *fh = [NSFileHandle fileHandleForLoggingAtPath:path mode:0644]; if (fh == nil) return nil; id me = [self standardLogger]; [me setWriter:fh]; return me; } @catch (id e) { // Ignored } return nil; } + (id)loggerWithWriter:(id)writer formatter:(id)formatter filter:(id)filter { return [[[self alloc] initWithWriter:writer formatter:formatter filter:filter] autorelease]; } + (id)logger { return [[[self alloc] init] autorelease]; } - (id)init { return [self initWithWriter:nil formatter:nil filter:nil]; } - (id)initWithWriter:(id)writer formatter:(id)formatter filter:(id)filter { if ((self = [super init])) { [self setWriter:writer]; [self setFormatter:formatter]; [self setFilter:filter]; } return self; } - (void)dealloc { // Unlikely, but |writer_| may be an NSFileHandle, which can throw @try { [formatter_ release]; [filter_ release]; [writer_ release]; } @catch (id e) { // Ignored } [super dealloc]; } - (id)writer { return [[writer_ retain] autorelease]; } - (void)setWriter:(id)writer { @synchronized(self) { [writer_ autorelease]; writer_ = nil; if (writer == nil) { // Try to use stdout, but don't trust NSFileHandle @try { writer_ = [[NSFileHandle fileHandleWithStandardOutput] retain]; } @catch (id e) { // Leave |writer_| nil } } else { writer_ = [writer retain]; } } } - (id)formatter { return [[formatter_ retain] autorelease]; } - (void)setFormatter:(id)formatter { @synchronized(self) { [formatter_ autorelease]; formatter_ = nil; if (formatter == nil) { @try { formatter_ = [[GTMLogBasicFormatter alloc] init]; } @catch (id e) { // Leave |formatter_| nil } } else { formatter_ = [formatter retain]; } } } - (id)filter { return [[filter_ retain] autorelease]; } - (void)setFilter:(id)filter { @synchronized(self) { [filter_ autorelease]; filter_ = nil; if (filter == nil) { @try { filter_ = [[GTMLogNoFilter alloc] init]; } @catch (id e) { // Leave |filter_| nil } } else { filter_ = [filter retain]; } } } - (void)logDebug:(NSString *)fmt, ... { va_list args; va_start(args, fmt); [self logInternalFunc:NULL format:fmt valist:args level:kGTMLoggerLevelDebug]; va_end(args); } - (void)logInfo:(NSString *)fmt, ... { va_list args; va_start(args, fmt); [self logInternalFunc:NULL format:fmt valist:args level:kGTMLoggerLevelInfo]; va_end(args); } - (void)logError:(NSString *)fmt, ... { va_list args; va_start(args, fmt); [self logInternalFunc:NULL format:fmt valist:args level:kGTMLoggerLevelError]; va_end(args); } - (void)logAssert:(NSString *)fmt, ... { va_list args; va_start(args, fmt); [self logInternalFunc:NULL format:fmt valist:args level:kGTMLoggerLevelAssert]; va_end(args); } @end // GTMLogger @implementation GTMLogger (GTMLoggerMacroHelpers) - (void)logFuncDebug:(const char *)func msg:(NSString *)fmt, ... { va_list args; va_start(args, fmt); [self logInternalFunc:func format:fmt valist:args level:kGTMLoggerLevelDebug]; va_end(args); } - (void)logFuncInfo:(const char *)func msg:(NSString *)fmt, ... { va_list args; va_start(args, fmt); [self logInternalFunc:func format:fmt valist:args level:kGTMLoggerLevelInfo]; va_end(args); } - (void)logFuncError:(const char *)func msg:(NSString *)fmt, ... { va_list args; va_start(args, fmt); [self logInternalFunc:func format:fmt valist:args level:kGTMLoggerLevelError]; va_end(args); } - (void)logFuncAssert:(const char *)func msg:(NSString *)fmt, ... { va_list args; va_start(args, fmt); [self logInternalFunc:func format:fmt valist:args level:kGTMLoggerLevelAssert]; va_end(args); } @end // GTMLoggerMacroHelpers @implementation GTMLogger (PrivateMethods) - (void)logInternalFunc:(const char *)func format:(NSString *)fmt valist:(va_list)args level:(GTMLoggerLevel)level { // Primary point where logging happens, logging should never throw, catch // everything. @try { NSString *fname = func ? [NSString stringWithUTF8String:func] : nil; NSString *msg = [formatter_ stringForFunc:fname withFormat:fmt valist:args level:level]; if (msg && [filter_ filterAllowsMessage:msg level:level]) [writer_ logMessage:msg level:level]; } @catch (id e) { // Ignored } } @end // PrivateMethods @implementation NSFileHandle (GTMFileHandleLogWriter) + (id)fileHandleForLoggingAtPath:(NSString *)path mode:(mode_t)mode { int fd = -1; if (path) { int flags = O_WRONLY | O_APPEND | O_CREAT; fd = open([path fileSystemRepresentation], flags, mode); } if (fd == -1) return nil; return [[[self alloc] initWithFileDescriptor:fd closeOnDealloc:YES] autorelease]; } - (void)logMessage:(NSString *)msg level:(GTMLoggerLevel)level { @synchronized(self) { // Closed pipes should not generate exceptions in our caller. Catch here // as well [GTMLogger logInternalFunc:...] so that an exception in this // writer does not prevent other writers from having a chance. @try { NSString *line = [NSString stringWithFormat:@"%@\n", msg]; [self writeData:[line dataUsingEncoding:NSUTF8StringEncoding]]; } @catch (id e) { // Ignored } } } @end // GTMFileHandleLogWriter @implementation NSArray (GTMArrayCompositeLogWriter) - (void)logMessage:(NSString *)msg level:(GTMLoggerLevel)level { @synchronized(self) { id child = nil; GTM_FOREACH_OBJECT(child, self) { if ([child conformsToProtocol:@protocol(GTMLogWriter)]) [child logMessage:msg level:level]; } } } @end // GTMArrayCompositeLogWriter @implementation GTMLogger (GTMLoggerLogWriter) - (void)logMessage:(NSString *)msg level:(GTMLoggerLevel)level { switch (level) { case kGTMLoggerLevelDebug: [self logDebug:@"%@", msg]; break; case kGTMLoggerLevelInfo: [self logInfo:@"%@", msg]; break; case kGTMLoggerLevelError: [self logError:@"%@", msg]; break; case kGTMLoggerLevelAssert: [self logAssert:@"%@", msg]; break; default: // Ignore the message. break; } } @end // GTMLoggerLogWriter @implementation GTMLogBasicFormatter - (NSString *)prettyNameForFunc:(NSString *)func { NSString *name = [func stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]]; NSString *function = @"(unknown)"; if ([name length]) { if (// Objective C __func__ and __PRETTY_FUNCTION__ [name hasPrefix:@"-["] || [name hasPrefix:@"+["] || // C++ __PRETTY_FUNCTION__ and other preadorned formats [name hasSuffix:@")"]) { function = name; } else { // Assume C99 __func__ function = [NSString stringWithFormat:@"%@()", name]; } } return function; } - (NSString *)stringForFunc:(NSString *)func withFormat:(NSString *)fmt valist:(va_list)args level:(GTMLoggerLevel)level { // Performance note: We may want to do a quick check here to see if |fmt| // contains a '%', and if not, simply return 'fmt'. if (!(fmt && args)) return nil; return [[[NSString alloc] initWithFormat:fmt arguments:args] autorelease]; } @end // GTMLogBasicFormatter @implementation GTMLogStandardFormatter - (id)init { if ((self = [super init])) { dateFormatter_ = [[NSDateFormatter alloc] init]; [dateFormatter_ setFormatterBehavior:NSDateFormatterBehavior10_4]; [dateFormatter_ setDateFormat:@"yyyy-MM-dd HH:mm:ss.SSS"]; pname_ = [[[NSProcessInfo processInfo] processName] copy]; pid_ = [[NSProcessInfo processInfo] processIdentifier]; if (!(dateFormatter_ && pname_)) { [self release]; return nil; } } return self; } - (void)dealloc { [dateFormatter_ release]; [pname_ release]; [super dealloc]; } - (NSString *)stringForFunc:(NSString *)func withFormat:(NSString *)fmt valist:(va_list)args level:(GTMLoggerLevel)level { NSString *tstamp = nil; @synchronized (dateFormatter_) { tstamp = [dateFormatter_ stringFromDate:[NSDate date]]; } return [NSString stringWithFormat:@"%@ %@[%d/%p] [lvl=%d] %@ %@", tstamp, pname_, pid_, pthread_self(), level, [self prettyNameForFunc:func], // |super| has guard for nil |fmt| and |args| [super stringForFunc:func withFormat:fmt valist:args level:level]]; } @end // GTMLogStandardFormatter @implementation GTMLogLevelFilter // Check the environment and the user preferences for the GTMVerboseLogging key // to see if verbose logging has been enabled. The environment variable will // override the defaults setting, so check the environment first. // COV_NF_START static BOOL IsVerboseLoggingEnabled(void) { static NSString *const kVerboseLoggingKey = @"GTMVerboseLogging"; NSString *value = [[[NSProcessInfo processInfo] environment] objectForKey:kVerboseLoggingKey]; if (value) { // Emulate [NSString boolValue] for pre-10.5 value = [value stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]]; if ([[value uppercaseString] hasPrefix:@"Y"] || [[value uppercaseString] hasPrefix:@"T"] || [value intValue]) { return YES; } else { return NO; } } return [[NSUserDefaults standardUserDefaults] boolForKey:kVerboseLoggingKey]; } // COV_NF_END // In DEBUG builds, log everything. If we're not in a debug build we'll assume // that we're in a Release build. - (BOOL)filterAllowsMessage:(NSString *)msg level:(GTMLoggerLevel)level { #if defined(DEBUG) && DEBUG return YES; #endif BOOL allow = YES; switch (level) { case kGTMLoggerLevelDebug: allow = NO; break; case kGTMLoggerLevelInfo: allow = IsVerboseLoggingEnabled(); break; case kGTMLoggerLevelError: allow = YES; break; case kGTMLoggerLevelAssert: allow = YES; break; default: allow = YES; break; } return allow; } @end // GTMLogLevelFilter @implementation GTMLogNoFilter - (BOOL)filterAllowsMessage:(NSString *)msg level:(GTMLoggerLevel)level { return YES; // Allow everything through } @end // GTMLogNoFilter @implementation GTMLogAllowedLevelFilter // Private designated initializer - (id)initWithAllowedLevels:(NSIndexSet *)levels { self = [super init]; if (self != nil) { allowedLevels_ = [levels retain]; // Cap min/max level if (!allowedLevels_ || // NSIndexSet is unsigned so only check the high bound, but need to // check both first and last index because NSIndexSet appears to allow // wraparound. ([allowedLevels_ firstIndex] > kGTMLoggerLevelAssert) || ([allowedLevels_ lastIndex] > kGTMLoggerLevelAssert)) { [self release]; return nil; } } return self; } - (id)init { // Allow all levels in default init return [self initWithAllowedLevels:[NSIndexSet indexSetWithIndexesInRange: NSMakeRange(kGTMLoggerLevelUnknown, (kGTMLoggerLevelAssert - kGTMLoggerLevelUnknown + 1))]]; } - (void)dealloc { [allowedLevels_ release]; [super dealloc]; } - (BOOL)filterAllowsMessage:(NSString *)msg level:(GTMLoggerLevel)level { return [allowedLevels_ containsIndex:level]; } @end // GTMLogAllowedLevelFilter @implementation GTMLogMininumLevelFilter - (id)initWithMinimumLevel:(GTMLoggerLevel)level { return [super initWithAllowedLevels:[NSIndexSet indexSetWithIndexesInRange: NSMakeRange(level, (kGTMLoggerLevelAssert - level + 1))]]; } @end // GTMLogMininumLevelFilter @implementation GTMLogMaximumLevelFilter - (id)initWithMaximumLevel:(GTMLoggerLevel)level { return [super initWithAllowedLevels:[NSIndexSet indexSetWithIndexesInRange: NSMakeRange(kGTMLoggerLevelUnknown, level + 1)]]; } @end // GTMLogMaximumLevelFilter #if !defined(__clang__) && (__GNUC__*10+__GNUC_MINOR__ >= 42) // See comment at top of file. #pragma GCC diagnostic error "-Wmissing-format-attribute" #endif // !__clang__