| // |
| // 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 <fcntl.h> |
| #import <unistd.h> |
| #import <stdlib.h> |
| #import <pthread.h> |
| |
| |
| #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<GTMLogWriter> writer = [NSFileHandle fileHandleWithStandardOutput]; |
| id<GTMLogFormatter> fr = [[[GTMLogStandardFormatter alloc] init] |
| autorelease]; |
| id<GTMLogFilter> 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<GTMLogWriter>)writer |
| formatter:(id<GTMLogFormatter>)formatter |
| filter:(id<GTMLogFilter>)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<GTMLogWriter>)writer |
| formatter:(id<GTMLogFormatter>)formatter |
| filter:(id<GTMLogFilter>)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<GTMLogWriter>)writer { |
| return [[writer_ retain] autorelease]; |
| } |
| |
| - (void)setWriter:(id<GTMLogWriter>)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<GTMLogFormatter>)formatter { |
| return [[formatter_ retain] autorelease]; |
| } |
| |
| - (void)setFormatter:(id<GTMLogFormatter>)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<GTMLogFilter>)filter { |
| return [[filter_ retain] autorelease]; |
| } |
| |
| - (void)setFilter:(id<GTMLogFilter>)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<GTMLogWriter> child = nil; |
| for (child in 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 |
| |
| static NSString *const kVerboseLoggingKey = @"GTMVerboseLogging"; |
| |
| // 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(NSUserDefaults *userDefaults) { |
| 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 [userDefaults boolForKey:kVerboseLoggingKey]; |
| } |
| // COV_NF_END |
| |
| @implementation GTMLogLevelFilter |
| |
| - (id)init { |
| self = [super init]; |
| if (self) { |
| // Keep a reference to standardUserDefaults, avoiding a crash if client code calls |
| // "NSUserDefaults resetStandardUserDefaults" which releases it from memory. We are still |
| // notified of changes through our instance. Note: resetStandardUserDefaults does not actually |
| // clear settings: |
| // https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSUserDefaults_Class/index.html#//apple_ref/occ/clm/NSUserDefaults/resetStandardUserDefaults |
| // and so should only be called in test code if necessary. |
| userDefaults_ = [[NSUserDefaults standardUserDefaults] retain]; |
| [userDefaults_ addObserver:self |
| forKeyPath:kVerboseLoggingKey |
| options:NSKeyValueObservingOptionNew |
| context:nil]; |
| |
| verboseLoggingEnabled_ = IsVerboseLoggingEnabled(userDefaults_); |
| } |
| |
| return self; |
| } |
| |
| - (void)dealloc { |
| [userDefaults_ removeObserver:self forKeyPath:kVerboseLoggingKey]; |
| [userDefaults_ release]; |
| |
| [super dealloc]; |
| } |
| |
| // 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 = verboseLoggingEnabled_; |
| break; |
| case kGTMLoggerLevelError: |
| allow = YES; |
| break; |
| case kGTMLoggerLevelAssert: |
| allow = YES; |
| break; |
| default: |
| allow = YES; |
| break; |
| } |
| |
| return allow; |
| } |
| |
| - (void)observeValueForKeyPath:(NSString *)keyPath |
| ofObject:(id)object |
| change:(NSDictionary *)change |
| context:(void *)context |
| { |
| if([keyPath isEqual:kVerboseLoggingKey]) { |
| verboseLoggingEnabled_ = IsVerboseLoggingEnabled(userDefaults_); |
| } |
| } |
| |
| @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__ |