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