1//
2//  GTMLogger.m
3//
4//  Copyright 2007-2008 Google Inc.
5//
6//  Licensed under the Apache License, Version 2.0 (the "License"); you may not
7//  use this file except in compliance with the License.  You may obtain a copy
8//  of the License at
9//
10//  http://www.apache.org/licenses/LICENSE-2.0
11//
12//  Unless required by applicable law or agreed to in writing, software
13//  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14//  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
15//  License for the specific language governing permissions and limitations under
16//  the License.
17//
18
19#import "GTMLogger.h"
20#import <fcntl.h>
21#import <unistd.h>
22#import <stdlib.h>
23#import <pthread.h>
24
25
26#if !defined(__clang__) && (__GNUC__*10+__GNUC_MINOR__ >= 42)
27// Some versions of GCC (4.2 and below AFAIK) aren't great about supporting
28// -Wmissing-format-attribute
29// when the function is anything more complex than foo(NSString *fmt, ...).
30// You see the error inside the function when you turn ... into va_args and
31// attempt to call another function (like vsprintf for example).
32// So we just shut off the warning for this file. We reenable it at the end.
33#pragma GCC diagnostic ignored "-Wmissing-format-attribute"
34#endif  // !__clang__
35
36// Reference to the shared GTMLogger instance. This is not a singleton, it's
37// just an easy reference to one shared instance.
38static GTMLogger *gSharedLogger = nil;
39
40
41@implementation GTMLogger
42
43// Returns a pointer to the shared logger instance. If none exists, a standard
44// logger is created and returned.
45+ (id)sharedLogger {
46  @synchronized(self) {
47    if (gSharedLogger == nil) {
48      gSharedLogger = [[self standardLogger] retain];
49    }
50  }
51  return [[gSharedLogger retain] autorelease];
52}
53
54+ (void)setSharedLogger:(GTMLogger *)logger {
55  @synchronized(self) {
56    [gSharedLogger autorelease];
57    gSharedLogger = [logger retain];
58  }
59}
60
61+ (id)standardLogger {
62  // Don't trust NSFileHandle not to throw
63  @try {
64    id<GTMLogWriter> writer = [NSFileHandle fileHandleWithStandardOutput];
65    id<GTMLogFormatter> fr = [[[GTMLogStandardFormatter alloc] init]
66                                 autorelease];
67    id<GTMLogFilter> filter = [[[GTMLogLevelFilter alloc] init] autorelease];
68    return [[[self alloc] initWithWriter:writer
69                               formatter:fr
70                                  filter:filter] autorelease];
71  }
72  @catch (id e) {
73    // Ignored
74  }
75  return nil;
76}
77
78+ (id)standardLoggerWithStderr {
79  // Don't trust NSFileHandle not to throw
80  @try {
81    id me = [self standardLogger];
82    [me setWriter:[NSFileHandle fileHandleWithStandardError]];
83    return me;
84  }
85  @catch (id e) {
86    // Ignored
87  }
88  return nil;
89}
90
91+ (id)standardLoggerWithStdoutAndStderr {
92  // We're going to take advantage of the GTMLogger to GTMLogWriter adaptor
93  // and create a composite logger that an outer "standard" logger can use
94  // as a writer. Our inner loggers should apply no formatting since the main
95  // logger does that and we want the caller to be able to change formatters
96  // or add writers without knowing the inner structure of our composite.
97
98  // Don't trust NSFileHandle not to throw
99  @try {
100    GTMLogBasicFormatter *formatter = [[[GTMLogBasicFormatter alloc] init]
101                                          autorelease];
102    GTMLogger *stdoutLogger =
103        [self loggerWithWriter:[NSFileHandle fileHandleWithStandardOutput]
104                     formatter:formatter
105                        filter:[[[GTMLogMaximumLevelFilter alloc]
106                                  initWithMaximumLevel:kGTMLoggerLevelInfo]
107                                      autorelease]];
108    GTMLogger *stderrLogger =
109        [self loggerWithWriter:[NSFileHandle fileHandleWithStandardError]
110                     formatter:formatter
111                        filter:[[[GTMLogMininumLevelFilter alloc]
112                                  initWithMinimumLevel:kGTMLoggerLevelError]
113                                      autorelease]];
114    GTMLogger *compositeWriter =
115        [self loggerWithWriter:[NSArray arrayWithObjects:
116                                   stdoutLogger, stderrLogger, nil]
117                     formatter:formatter
118                        filter:[[[GTMLogNoFilter alloc] init] autorelease]];
119    GTMLogger *outerLogger = [self standardLogger];
120    [outerLogger setWriter:compositeWriter];
121    return outerLogger;
122  }
123  @catch (id e) {
124    // Ignored
125  }
126  return nil;
127}
128
129+ (id)standardLoggerWithPath:(NSString *)path {
130  @try {
131    NSFileHandle *fh = [NSFileHandle fileHandleForLoggingAtPath:path mode:0644];
132    if (fh == nil) return nil;
133    id me = [self standardLogger];
134    [me setWriter:fh];
135    return me;
136  }
137  @catch (id e) {
138    // Ignored
139  }
140  return nil;
141}
142
143+ (id)loggerWithWriter:(id<GTMLogWriter>)writer
144             formatter:(id<GTMLogFormatter>)formatter
145                filter:(id<GTMLogFilter>)filter {
146  return [[[self alloc] initWithWriter:writer
147                             formatter:formatter
148                                filter:filter] autorelease];
149}
150
151+ (id)logger {
152  return [[[self alloc] init] autorelease];
153}
154
155- (id)init {
156  return [self initWithWriter:nil formatter:nil filter:nil];
157}
158
159- (id)initWithWriter:(id<GTMLogWriter>)writer
160           formatter:(id<GTMLogFormatter>)formatter
161              filter:(id<GTMLogFilter>)filter {
162  if ((self = [super init])) {
163    [self setWriter:writer];
164    [self setFormatter:formatter];
165    [self setFilter:filter];
166  }
167  return self;
168}
169
170- (void)dealloc {
171  // Unlikely, but |writer_| may be an NSFileHandle, which can throw
172  @try {
173    [formatter_ release];
174    [filter_ release];
175    [writer_ release];
176  }
177  @catch (id e) {
178    // Ignored
179  }
180  [super dealloc];
181}
182
183- (id<GTMLogWriter>)writer {
184  return [[writer_ retain] autorelease];
185}
186
187- (void)setWriter:(id<GTMLogWriter>)writer {
188  @synchronized(self) {
189    [writer_ autorelease];
190    writer_ = nil;
191    if (writer == nil) {
192      // Try to use stdout, but don't trust NSFileHandle
193      @try {
194        writer_ = [[NSFileHandle fileHandleWithStandardOutput] retain];
195      }
196      @catch (id e) {
197        // Leave |writer_| nil
198      }
199    } else {
200      writer_ = [writer retain];
201    }
202  }
203}
204
205- (id<GTMLogFormatter>)formatter {
206  return [[formatter_ retain] autorelease];
207}
208
209- (void)setFormatter:(id<GTMLogFormatter>)formatter {
210  @synchronized(self) {
211    [formatter_ autorelease];
212    formatter_ = nil;
213    if (formatter == nil) {
214      @try {
215        formatter_ = [[GTMLogBasicFormatter alloc] init];
216      }
217      @catch (id e) {
218        // Leave |formatter_| nil
219      }
220    } else {
221      formatter_ = [formatter retain];
222    }
223  }
224}
225
226- (id<GTMLogFilter>)filter {
227  return [[filter_ retain] autorelease];
228}
229
230- (void)setFilter:(id<GTMLogFilter>)filter {
231  @synchronized(self) {
232    [filter_ autorelease];
233    filter_ = nil;
234    if (filter == nil) {
235      @try {
236        filter_ = [[GTMLogNoFilter alloc] init];
237      }
238      @catch (id e) {
239        // Leave |filter_| nil
240      }
241    } else {
242      filter_ = [filter retain];
243    }
244  }
245}
246
247- (void)logDebug:(NSString *)fmt, ... {
248  va_list args;
249  va_start(args, fmt);
250  [self logInternalFunc:NULL format:fmt valist:args level:kGTMLoggerLevelDebug];
251  va_end(args);
252}
253
254- (void)logInfo:(NSString *)fmt, ... {
255  va_list args;
256  va_start(args, fmt);
257  [self logInternalFunc:NULL format:fmt valist:args level:kGTMLoggerLevelInfo];
258  va_end(args);
259}
260
261- (void)logError:(NSString *)fmt, ... {
262  va_list args;
263  va_start(args, fmt);
264  [self logInternalFunc:NULL format:fmt valist:args level:kGTMLoggerLevelError];
265  va_end(args);
266}
267
268- (void)logAssert:(NSString *)fmt, ... {
269  va_list args;
270  va_start(args, fmt);
271  [self logInternalFunc:NULL format:fmt valist:args level:kGTMLoggerLevelAssert];
272  va_end(args);
273}
274
275@end  // GTMLogger
276
277@implementation GTMLogger (GTMLoggerMacroHelpers)
278
279- (void)logFuncDebug:(const char *)func msg:(NSString *)fmt, ... {
280  va_list args;
281  va_start(args, fmt);
282  [self logInternalFunc:func format:fmt valist:args level:kGTMLoggerLevelDebug];
283  va_end(args);
284}
285
286- (void)logFuncInfo:(const char *)func msg:(NSString *)fmt, ... {
287  va_list args;
288  va_start(args, fmt);
289  [self logInternalFunc:func format:fmt valist:args level:kGTMLoggerLevelInfo];
290  va_end(args);
291}
292
293- (void)logFuncError:(const char *)func msg:(NSString *)fmt, ... {
294  va_list args;
295  va_start(args, fmt);
296  [self logInternalFunc:func format:fmt valist:args level:kGTMLoggerLevelError];
297  va_end(args);
298}
299
300- (void)logFuncAssert:(const char *)func msg:(NSString *)fmt, ... {
301  va_list args;
302  va_start(args, fmt);
303  [self logInternalFunc:func format:fmt valist:args level:kGTMLoggerLevelAssert];
304  va_end(args);
305}
306
307@end  // GTMLoggerMacroHelpers
308
309@implementation GTMLogger (PrivateMethods)
310
311- (void)logInternalFunc:(const char *)func
312                 format:(NSString *)fmt
313                 valist:(va_list)args
314                  level:(GTMLoggerLevel)level {
315  // Primary point where logging happens, logging should never throw, catch
316  // everything.
317  @try {
318    NSString *fname = func ? [NSString stringWithUTF8String:func] : nil;
319    NSString *msg = [formatter_ stringForFunc:fname
320                                   withFormat:fmt
321                                       valist:args
322                                        level:level];
323    if (msg && [filter_ filterAllowsMessage:msg level:level])
324      [writer_ logMessage:msg level:level];
325  }
326  @catch (id e) {
327    // Ignored
328  }
329}
330
331@end  // PrivateMethods
332
333
334@implementation NSFileHandle (GTMFileHandleLogWriter)
335
336+ (id)fileHandleForLoggingAtPath:(NSString *)path mode:(mode_t)mode {
337  int fd = -1;
338  if (path) {
339    int flags = O_WRONLY | O_APPEND | O_CREAT;
340    fd = open([path fileSystemRepresentation], flags, mode);
341  }
342  if (fd == -1) return nil;
343  return [[[self alloc] initWithFileDescriptor:fd
344                                closeOnDealloc:YES] autorelease];
345}
346
347- (void)logMessage:(NSString *)msg level:(GTMLoggerLevel)level {
348  @synchronized(self) {
349    // Closed pipes should not generate exceptions in our caller. Catch here
350    // as well [GTMLogger logInternalFunc:...] so that an exception in this
351    // writer does not prevent other writers from having a chance.
352    @try {
353      NSString *line = [NSString stringWithFormat:@"%@\n", msg];
354      [self writeData:[line dataUsingEncoding:NSUTF8StringEncoding]];
355    }
356    @catch (id e) {
357      // Ignored
358    }
359  }
360}
361
362@end  // GTMFileHandleLogWriter
363
364
365@implementation NSArray (GTMArrayCompositeLogWriter)
366
367- (void)logMessage:(NSString *)msg level:(GTMLoggerLevel)level {
368  @synchronized(self) {
369    id<GTMLogWriter> child = nil;
370    GTM_FOREACH_OBJECT(child, self) {
371      if ([child conformsToProtocol:@protocol(GTMLogWriter)])
372        [child logMessage:msg level:level];
373    }
374  }
375}
376
377@end  // GTMArrayCompositeLogWriter
378
379
380@implementation GTMLogger (GTMLoggerLogWriter)
381
382- (void)logMessage:(NSString *)msg level:(GTMLoggerLevel)level {
383  switch (level) {
384    case kGTMLoggerLevelDebug:
385      [self logDebug:@"%@", msg];
386      break;
387    case kGTMLoggerLevelInfo:
388      [self logInfo:@"%@", msg];
389      break;
390    case kGTMLoggerLevelError:
391      [self logError:@"%@", msg];
392      break;
393    case kGTMLoggerLevelAssert:
394      [self logAssert:@"%@", msg];
395      break;
396    default:
397      // Ignore the message.
398      break;
399  }
400}
401
402@end  // GTMLoggerLogWriter
403
404
405@implementation GTMLogBasicFormatter
406
407- (NSString *)prettyNameForFunc:(NSString *)func {
408  NSString *name = [func stringByTrimmingCharactersInSet:
409                     [NSCharacterSet whitespaceAndNewlineCharacterSet]];
410  NSString *function = @"(unknown)";
411  if ([name length]) {
412    if (// Objective C __func__ and __PRETTY_FUNCTION__
413        [name hasPrefix:@"-["] || [name hasPrefix:@"+["] ||
414        // C++ __PRETTY_FUNCTION__ and other preadorned formats
415        [name hasSuffix:@")"]) {
416      function = name;
417    } else {
418      // Assume C99 __func__
419      function = [NSString stringWithFormat:@"%@()", name];
420    }
421  }
422  return function;
423}
424
425- (NSString *)stringForFunc:(NSString *)func
426                 withFormat:(NSString *)fmt
427                     valist:(va_list)args
428                      level:(GTMLoggerLevel)level {
429  // Performance note: We may want to do a quick check here to see if |fmt|
430  // contains a '%', and if not, simply return 'fmt'.
431  if (!(fmt && args)) return nil;
432  return [[[NSString alloc] initWithFormat:fmt arguments:args] autorelease];
433}
434
435@end  // GTMLogBasicFormatter
436
437
438@implementation GTMLogStandardFormatter
439
440- (id)init {
441  if ((self = [super init])) {
442    dateFormatter_ = [[NSDateFormatter alloc] init];
443    [dateFormatter_ setFormatterBehavior:NSDateFormatterBehavior10_4];
444    [dateFormatter_ setDateFormat:@"yyyy-MM-dd HH:mm:ss.SSS"];
445    pname_ = [[[NSProcessInfo processInfo] processName] copy];
446    pid_ = [[NSProcessInfo processInfo] processIdentifier];
447    if (!(dateFormatter_ && pname_)) {
448      [self release];
449      return nil;
450    }
451  }
452  return self;
453}
454
455- (void)dealloc {
456  [dateFormatter_ release];
457  [pname_ release];
458  [super dealloc];
459}
460
461- (NSString *)stringForFunc:(NSString *)func
462                 withFormat:(NSString *)fmt
463                     valist:(va_list)args
464                      level:(GTMLoggerLevel)level {
465  NSString *tstamp = nil;
466  @synchronized (dateFormatter_) {
467    tstamp = [dateFormatter_ stringFromDate:[NSDate date]];
468  }
469  return [NSString stringWithFormat:@"%@ %@[%d/%p] [lvl=%d] %@ %@",
470           tstamp, pname_, pid_, pthread_self(),
471           level, [self prettyNameForFunc:func],
472           // |super| has guard for nil |fmt| and |args|
473           [super stringForFunc:func withFormat:fmt valist:args level:level]];
474}
475
476@end  // GTMLogStandardFormatter
477
478
479@implementation GTMLogLevelFilter
480
481// Check the environment and the user preferences for the GTMVerboseLogging key
482// to see if verbose logging has been enabled. The environment variable will
483// override the defaults setting, so check the environment first.
484// COV_NF_START
485static BOOL IsVerboseLoggingEnabled(void) {
486  static NSString *const kVerboseLoggingKey = @"GTMVerboseLogging";
487  NSString *value = [[[NSProcessInfo processInfo] environment]
488                        objectForKey:kVerboseLoggingKey];
489  if (value) {
490    // Emulate [NSString boolValue] for pre-10.5
491    value = [value stringByTrimmingCharactersInSet:
492                [NSCharacterSet whitespaceAndNewlineCharacterSet]];
493    if ([[value uppercaseString] hasPrefix:@"Y"] ||
494        [[value uppercaseString] hasPrefix:@"T"] ||
495        [value intValue]) {
496      return YES;
497    } else {
498      return NO;
499    }
500  }
501  return [[NSUserDefaults standardUserDefaults] boolForKey:kVerboseLoggingKey];
502}
503// COV_NF_END
504
505// In DEBUG builds, log everything. If we're not in a debug build we'll assume
506// that we're in a Release build.
507- (BOOL)filterAllowsMessage:(NSString *)msg level:(GTMLoggerLevel)level {
508#if defined(DEBUG) && DEBUG
509  return YES;
510#endif
511
512  BOOL allow = YES;
513
514  switch (level) {
515    case kGTMLoggerLevelDebug:
516      allow = NO;
517      break;
518    case kGTMLoggerLevelInfo:
519      allow = IsVerboseLoggingEnabled();
520      break;
521    case kGTMLoggerLevelError:
522      allow = YES;
523      break;
524    case kGTMLoggerLevelAssert:
525      allow = YES;
526      break;
527    default:
528      allow = YES;
529      break;
530  }
531
532  return allow;
533}
534
535@end  // GTMLogLevelFilter
536
537
538@implementation GTMLogNoFilter
539
540- (BOOL)filterAllowsMessage:(NSString *)msg level:(GTMLoggerLevel)level {
541  return YES;  // Allow everything through
542}
543
544@end  // GTMLogNoFilter
545
546
547@implementation GTMLogAllowedLevelFilter
548
549// Private designated initializer
550- (id)initWithAllowedLevels:(NSIndexSet *)levels {
551  self = [super init];
552  if (self != nil) {
553    allowedLevels_ = [levels retain];
554    // Cap min/max level
555    if (!allowedLevels_ ||
556        // NSIndexSet is unsigned so only check the high bound, but need to
557        // check both first and last index because NSIndexSet appears to allow
558        // wraparound.
559        ([allowedLevels_ firstIndex] > kGTMLoggerLevelAssert) ||
560        ([allowedLevels_ lastIndex] > kGTMLoggerLevelAssert)) {
561      [self release];
562      return nil;
563    }
564  }
565  return self;
566}
567
568- (id)init {
569  // Allow all levels in default init
570  return [self initWithAllowedLevels:[NSIndexSet indexSetWithIndexesInRange:
571             NSMakeRange(kGTMLoggerLevelUnknown,
572                 (kGTMLoggerLevelAssert - kGTMLoggerLevelUnknown + 1))]];
573}
574
575- (void)dealloc {
576  [allowedLevels_ release];
577  [super dealloc];
578}
579
580- (BOOL)filterAllowsMessage:(NSString *)msg level:(GTMLoggerLevel)level {
581  return [allowedLevels_ containsIndex:level];
582}
583
584@end  // GTMLogAllowedLevelFilter
585
586
587@implementation GTMLogMininumLevelFilter
588
589- (id)initWithMinimumLevel:(GTMLoggerLevel)level {
590  return [super initWithAllowedLevels:[NSIndexSet indexSetWithIndexesInRange:
591             NSMakeRange(level,
592                         (kGTMLoggerLevelAssert - level + 1))]];
593}
594
595@end  // GTMLogMininumLevelFilter
596
597
598@implementation GTMLogMaximumLevelFilter
599
600- (id)initWithMaximumLevel:(GTMLoggerLevel)level {
601  return [super initWithAllowedLevels:[NSIndexSet indexSetWithIndexesInRange:
602             NSMakeRange(kGTMLoggerLevelUnknown, level + 1)]];
603}
604
605@end  // GTMLogMaximumLevelFilter
606
607#if !defined(__clang__) && (__GNUC__*10+__GNUC_MINOR__ >= 42)
608// See comment at top of file.
609#pragma GCC diagnostic error "-Wmissing-format-attribute"
610#endif  // !__clang__
611
612