introduce

CocoaLumberjack is a fast & simple, yet powerful & flexible logging framework for Mac and iOS.

Let’s start with the word lumberjack, which corresponds to the logo, a lumberjack. Never quite understood why this word was used. There are other sounds that use this word for log library. Lumberjack (lumberjack) : Lumberjack (lumberjack) : Lumberjack (lumberjack)

Writing this article, I recently stumbled upon the fact that it has so many hidden features, even though it was introduced in the project for many years. Then I took a look at the official demos and was stunned. So this article wants to introduce some of its design and 🤔 from the source code. It concludes with a look at the extensions it supports.

Document

As a library with a long history, its documents are very detailed, mainly divided into three levels:

  • Beginner level: instructions, custom log format, performance test, color output support, etc.
  • Intermediate: Lumberjack internal overview, how to customize custom logging context, Custrom Logger, Custom log levels, etc.
  • Advanced: dynamic change log levels, log file management (compression, upload).

Architecture

As usual, let’s preview the class diagram to get a general idea.

After combing through the brain diagram, I found that the official UML diagram is actually provided. But since the brain map is organized, I’ll post it at the end of the article.

The intuitive feeling of UML is that there are not many classes, but the functions are very perfect. Let’s take a look at them bit by bit.

DDLog

By default, you’ve experienced the Lumberjack API. If you’re completely unfamiliar with Lumberjack’s API, go ahead and get Start.

The two most important protocols, DDLoger and DDLogFormatter, are declared in the core ddlog.h file, and the DDLog class can be regarded as a manager that manages all registered loogers and formatters. These three are sufficient for normal projects. So we’re going to start with protocol, and then we’re going to end with DDLog.

Loggers

A logger is a class that does something with a log message. The lumberjack framework comes with several different loggers. (You can also create your own.) Loggers such as DDOSLogger can be used to duplicate the functionality of NSLog. And DDFileLogger can be used to write log messages to a log file.

Loggers are used to process log messages. What information is available in a DDLogMessage?

DDLogMessage

Used by the logging primitives. (And the macros use the logging primitives.)

Log Message is used for logging primitives, which are implemented through macros. What’s the meaning of primitives? It can be understood that the log message holds the context of a series of relevant environments in which the log is called. The word primitive didn’t make sense at first, but there was a primitive (not necessarily correct) in the computer that helped us understand the word.

What exactly are they storing?

@interface DDLogMessage : NSObject <NSCopying>
{
    // Direct accessors to be used only for performance
    @public
    NSString *_message;
    DDLogLevel _level;
    DDLogFlag _flag;
    NSInteger _context;
    NSString *_file;
    NSString *_fileName;
    NSString *_function;
    NSUInteger _line;
    id _tag;
    DDLogMessageOptions _options;
    NSDate * _timestamp;
    NSString *_threadID;
    NSString *_threadName;
    NSString *_queueLabel;
    NSUInteger _qos;
}
Copy the code

In this case, the instance variable is declared in front, so that the caller can access the variable directly without the getter, to improve access efficiency. Of course, the author also provides readonly @Property method.

Message, file, and function do not copy by default. You can use DDLogMessageOptions to control this:

typedef NS_OPTIONS(NSInteger, DDLogMessageOptions){
	 /// Use this to use a copy of the file path
    DDLogMessageCopyFile        = 1 << 0./// Use this to use a copy of the function name
    DDLogMessageCopyFunction    = 1 << 1./// Use this to use avoid a copy of the message
    DDLogMessageDontCopyMessage = 1 << 2
};
Copy the code

We know that we need to use copy when we operate on NSString, so that we can operate on it safely and immutable. Copy is not used here for Message, File, and function to avoid unnecessary allocations overhead. Since file and function are obtained through the __FILE__ and __FUNCTION__ macros, they are essentially a character constant, so they can do this. While messages are normally generated internally by DDlog, Lumberjack ensures that mesage cannot be modified. So official tips are as follows:

If you find need to manually create logMessage objects, there is one thing you should be aware of.

All I’m saying is, when you’re generating log messages manually, you need to be aware of the memory modification of these three parameters.

The log message internal implementation is relatively simple. Take the message field as an example:

BOOL copyMessage = (options & DDLogMessageDontCopyMessage) == 0;
_message = copyMessage ? [message copy] : message;
Copy the code

In addition, each logMessage will record the thread and queue that are currently invoked, as follows:

__uint64_t tid;
if (pthread_threadid_np(NULL, &tid) == 0) {
    _threadID = [[NSString alloc] initWithFormat:@"%llu", tid];
} else {
    _threadID = @"missing threadId";
}
_threadName   = NSThread.currentThread.name;
// Try to get the current queue's label
_queueLabel = [[NSString alloc] initWithFormat:@"%s", dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL)];
if (@available(macOS 10.10, iOS 8.0, *))
    _qos = (NSUInteger) qos_class_self();
Copy the code

DDLogLevel

Log levels are used to filter out logs. Used together with flags.

Each log mesage sets the corresponding log level, which is used to filter logs. Its definition is an enumeration:

typedef NS_ENUM(NSUInteger, DDLogLevel) {
    // No logs
    DDLogLevelOff       = 0.// Error logs only
    DDLogLevelError     = (DDLogFlagError), 
    // Error and warning logs
    DDLogLevelWarning   = (DDLogLevelError   | DDLogFlagWarning),
    // Error, warning and info logs
    DDLogLevelInfo      = (DDLogLevelWarning | DDLogFlagInfo), 
    // Error, warning, info and debug logs
    DDLogLevelDebug     = (DDLogLevelInfo    | DDLogFlagDebug), 
    // Error, warning, info, debug and verbose logs
    DDLogLevelVerbose   = (DDLogLevelDebug   | DDLogFlagVerbose), 
    // All logs (1... 11111).
    DDLogLevelAll       = NSUIntegerMax 
};
Copy the code

The loglevel is controlled by DDLogFlag, which is declared as follows:

typedef NS_OPTIONS(NSUInteger, DDLogFlag) {
    / / 0... 00001 DDLogFlagError
    DDLogFlagError      = (1 << 0),
    / / 0... 00010 DDLogFlagWarning
    DDLogFlagWarning    = (1 << 1),
    / / 0... 00100 DDLogFlagInfo
    DDLogFlagInfo       = (1 << 2),
    / / 0... 01000 DDLogFlagDebug
    DDLogFlagDebug      = (1 << 3),
    / / 0... 10000 DDLogFlagVerbose
    DDLogFlagVerbose    = (1 << 4)};Copy the code

These are the 5 levels that DDLog presets, which are basically sufficient for beginners. At the same time, users who have custom level requirements can easily do so through structured macros. See CustomLogLevels. Md.

The core is to clear the default level and then redefine it:

// First undefine the default stuff we don't want to use.
#undef DDLogError
#undef DDLogWarn
#undef DDLogInfo
#undef DDLogDebug
#undef DDLogVerbose.// Now define everything how we want it
#define LOG_FLAG_FATAL   (1 < < 0) / / 0... 000001 #define LOG_LEVEL_FATAL (LOG_FLAG_FATAL) // 0... 000001 #define LOG_FATAL (ddLogLevel & LOG_FLAG_FATAL ) #define DDLogFatal(frmt, ...) SYNC_LOG_OBJC_MAYBE(ddLogLevel, LOG_FLAG_FATAL, 0, frmt, ##__VA_ARGS__) ...Copy the code

In addition to redefining level, we can also extend level to meet our needs. Lumberjack uses a bitmask and only presets 5 bits, corresponding to 5 log flags.

LogLevel is an Int, which means that for 32-bit systems, there are 28 bits reserved for our levels, since the default level only takes 4 bits. There is more than enough space to expand. There are two official scenarios that need to be extended, see finegrainedlogging.md.

DDLoger

This protocol describes a basic logger behavior.

  • Basically, it can log messages, store a logFormatter plus a bunch of optional behaviors.
  • (i.e. flush, get its loggerQueue, get its name, …
@protocol DDLogger <NSObject>

- (void)logMessage:(DDLogMessage *)logMessage NS_SWIFT_NAME(log(message:));
@property (nonatomic, strong, nullable) id <DDLogFormatter> logFormatter;

@optional
- (void)didAddLogger;
- (void)didAddLoggerInQueue:(dispatch_queue_t)queue;
- (void)willRemoveLogger;
- (void)flush;

@property (nonatomic, DISPATCH_QUEUE_REFERENCE_TYPE, readonly) dispatch_queue_t loggerQueue;
@property (copy, nonatomic, readonly) DDLoggerName loggerName;

@end
Copy the code

There’s nothing to say about logMessage, logFormatter will be covered later. Focus on the optional methods and parameters above.

loggerQueue

Looking at the loggerQueue, since log printing is asynchronous, a dispatch_queue_t is assigned to each looger. If Logger does not provide a loggerQueue, DDLog will be generated for you based on the loggerName you specify.

didAddLogger

The didAddLogger method is used to notify the Logger that it has been added successfully, and this is done in the loggerQueue.

Similarly, didAddLoggerInQueue: and willRemoveLogger have similar purposes.

flush

Refresh log messages that exist in the queue and have not yet been processed. For example, the Database Logger may use I/O buffers to reduce the frequency of log storage, since disk I/O is time-consuming. In this case, the Logger may have log messages that are not processed in time.

DDLog flushLog to flush. FlushLog is automatically called when the application exits from ⚠️. Of course, as developers we can manually trigger the refresh in appropriate circumstances, but normally you don’t need to manually trigger it.

DDLogFormatter

Formatter allow you to format a log message before the logger logs it.

@protocol DDLogFormatter <NSObject>

@required
- (nullable NSString *)formatLogMessage:(DDLogMessage *)logMessage NS_SWIFT_NAME(format(message:));

@optional
- (void)didAddToLogger:(id <DDLogger>)logger;
- (void)didAddToLogger:(id <DDLogger>)logger inQueue:(dispatch_queue_t)queue;
- (void)willRemoveFromLogger:(id <DDLogger>)logger;

@end
Copy the code

formatLogMessage:

A Formatter can be added to any Logger, with the formatLogMessage: greatly increasing the freedom of logging. How do you understand that? We can return different results for the File Logger and console using formatLogMessage:. For example, the console system automatically adds a timestamp to the log, and we need to add the time when we write to the log file. We can also filter the corresponding log by returning nil as a filter.

didAddToLogger

A Formatter can be added to multiple Loggers. This method is used to notify the formatter when it is added. This method needs to be thread safe, otherwise a thread safety exception may occur.

Similarly, didAddToLogger: inQueue means format in a specified queue.

WillRemoveFromLogger is the notification when the Formatter has been removed.

DDLog

The main class, exposes all logging mechanisms, loggers, …

For most of the users, this class is hidden behind the logging functions like DDLogInfo

As the lumberjack management class, DDLog collects user logs and schedules them to different Loggers in a centralized manner to achieve different functions, such as console log and file log. Therefore, being a singleton is a must. So let’s look at what it does when it initializes.

Initialize

@interface DDLog () @property (nonatomic, strong) NSMutableArray *_loggers; @end @implementation DDLog static dispatch_queue_t _loggingQueue; static dispatch_group_t _loggingGroup; static dispatch_semaphore_t _queueSemaphore; static NSUInteger _numProcessors; .Copy the code

Above a few are private variables, _loggers since needless to say, any add/remove logger needs in loggingQueue/loggingThread.

_loggingQueue

The global log queue is used to ensure the order of FIFO operations. All loggers execute their logmessages in order:.

_loggingGroup

Each Logger is added with a log queue. Thus, recording behavior between Loggers is performed concurrently. The Dispatch group synchronizes the operations of all Loggers to ensure that the logging behavior completes smoothly.

_queueSemaphore

Prevents the queue being used from overbursting. Since most logging operations are asynchronous, it is possible for malicious threads to increase the number of logs that can affect normal logging behavior. The maximum limit is DDLOG_MAX_QUEUE_SIZE (1000), which means that when the number of queues exceeds the limit, the thread is actively blocked until the execution queue is lowered to a safe level.

For example, 💥 happened when you arbitrarily added log statements to a large loop.

_numProcessors

Record the number of processor cores to optimize for single-core situations.

As a static variable, its initialization is placed in initialize, as follows:

+ (void)initialize { static dispatch_once_t DDLogOnceToken; dispatch_once(&DDLogOnceToken, ^{ NSLogDebug(@"DDLog: Using grand central dispatch"); _loggingQueue = dispatch_queue_create("cocoa.lumberjack", NULL); _loggingGroup = dispatch_group_create(); void *nonNullValue = GlobalLoggingQueueIdentityKey; // Whatever, just not null dispatch_queue_set_specific(_loggingQueue, GlobalLoggingQueueIdentityKey, nonNullValue, NULL); _queueSemaphore = dispatch_semaphore_create(DDLOG_MAX_QUEUE_SIZE); // Figure out how many processors are available. // This may be used later for an optimization on uniprocessor machines.  _numProcessors = MAX([NSProcessInfo processInfo].processorCount, (NSUInteger) 1); NSLogDebug(@"DDLog: numProcessors = %@", @(_numProcessors)); }); }Copy the code

The above code, through dispatch_queue_set_specific for _loggingQueue added key: GlobalLoggingQueueIdentityKey as tags. This is then asserted by obtaining the flag from dispatch_get_specific before all internal methods are executed, ensuring that internal methods are dispatched in the global _loggingQueue.

Next, let’s look at the initialization of the DDLog instance, which only does two things:

  • Initialization of _loggers;
  • Try registering notifications to make sure the message in Logger is processed before the APP process ends;

Because Lumberjack supports all platforms and the command line, there are more conditions for notificationName:

#if TARGET_OS_IOS NSString *notificationName = UIApplicationWillTerminateNotification; #else NSString *notificationName = nil; // On Command Line Tool apps AppKit may not be available #if ! defined(DD_CLI) && __has_include(<AppKit/NSApplication.h>) if (NSApp) { notificationName = NSApplicationWillTerminateNotification; } #endif if (! notificationName) { // If there is no NSApp -> we are running Command Line Tool app. // In this case terminate notification wouldn't be fired, so we use workaround. __weak __auto_type weakSelf = self; atexit_b (^{ [weakSelf applicationWillTerminate:nil]; }); } #endif /* if TARGET_OS_IOS */Copy the code

Just a little bit, how does the command line listen for an exit? Here we use atexit

The atexit() function registers the given function to be called at program exit, whether via exit(3) or via return from the program’s main(). Functions so registered are called in reverse order; no arguments are passed.

That is, when the program exits, the system will actively call the callbacks registered through atexit. Multiple callbacks can be registered and executed in sequence.

DDLog fires flush when notified, which we will expand later.

if (notificationName) {
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(applicationWillTerminate:)
                                                 name:notificationName
                                               object:nil];
}

- (void)applicationWillTerminate:(NSNotification * __attribute__((unused)))notification {
    [self flushLog];
}
Copy the code

Logger Management

Logger operations include adding and deleting loggers.

AddLogger

DDLog provides multiple methods to add a Logger to convince:

+ (void)addLogger:(id <DDLogger>)logger;
- (void)addLogger:(id <DDLogger>)logger;
+ (void)addLogger:(id <DDLogger>)logger withLevel:(DDLogLevel)level;
- (void)addLogger:(id <DDLogger>)logger withLevel:(DDLogLevel)level;

- (void)addLogger:(id <DDLogger>)logger withLevel:(DDLogLevel)level {
    if (!logger) {
        return;
    }
    dispatch_async(_loggingQueue, ^{ @autoreleasepool {
        [self lt_addLogger:logger level:level];
    } });
}
Copy the code

After placing the _loggingQueue, it eventually goes to the lt_addLogger: level: method. The prefix lt is short for Lgging Thread. Check for deweighting before adding a Logger:

for (DDLoggerNode *node in self._loggers) { if (node->_logger == logger && node->_level == level) { // Exactly same logger already added, exit return; }}Copy the code

DDLoggerNode

@interface DDLoggerNode : NSObject
{
    // Direct accessors to be used only for performance
    @public
    id <DDLogger> _logger;
    DDLogLevel _level;
    dispatch_queue_t _loggerQueue;
}

+ (instancetype)nodeWithLogger:(id <DDLogger>)logger
                   loggerQueue:(dispatch_queue_t)loggerQueue
                         level:(DDLogLevel)level;
Copy the code

Private class that associates Logger, Level, and loggerQueue.

In DDLoggerNode initialization, MRC is compatible. A macro OS_OBJECT_USE_OBJC is used internally to distinguish whether GCD supports ARC or not. Before 6.0, objects in GCD did not support ARC, so before 6.0, OS_OBJECT_USE_OBJC was not available.

if (loggerQueue) { _loggerQueue = loggerQueue; #if ! OS_OBJECT_USE_OBJC dispatch_retain(loggerQueue); #endif }Copy the code

This is followed by the QueueIdentity assertion mentioned earlier:

NSAssert(dispatch_get_specific(GlobalLoggingQueueIdentityKey),
         @"This method should only be run on the logging thread/queue");
Copy the code

Prepare loggerQueue:

dispatch_queue_t loggerQueue = NULL;
if ([logger respondsToSelector:@selector(loggerQueue)]) {
    loggerQueue = logger.loggerQueue;
}

if (loggerQueue == nil) {
    const char *loggerQueueName = NULL;
    if ([logger respondsToSelector:@selector(loggerName)]) {
        loggerQueueName = logger.loggerName.UTF8String;
    }
    loggerQueue = dispatch_queue_create(loggerQueueName, NULL);
}
Copy the code

This code, is there any deja vu dry? This is the logic mentioned in the DDLogger Protocol declaration. If loggerQueue is provided by Logger, use it directly. Otherwise, it is created by loggerName.

Finally, create DDLoggerNode, add Logger, and send didAddLogger notifications.

DDLoggerNode *loggerNode = [DDLoggerNode nodeWithLogger:logger loggerQueue:loggerQueue level:level]; [self._loggers addObject:loggerNode]; if ([logger respondsToSelector:@selector(didAddLoggerInQueue:)]) { dispatch_async(loggerNode->_loggerQueue, ^{ @autoreleasepool { [logger didAddLoggerInQueue:loggerNode->_loggerQueue]; }}); } else if ([logger respondsToSelector:@selector(didAddLogger)]) { dispatch_async(loggerNode->_loggerQueue, ^{ @autoreleasepool { [logger didAddLogger]; }}); }Copy the code

RemoveLogger

Like addLogger, removeLogger provides instance and class methods. Class methods are finally closed to instance methods via sharedInstance:

- (void)removeLogger:(id <DDLogger>)logger {
    if (!logger) {
        return;
    }
    dispatch_async(_loggingQueue, ^{ @autoreleasepool {
        [self lt_removeLogger:logger];
    } });
}
Copy the code

-[DDLog lt_removeLogger:]

LoggingQueue, loggerNode, loggerNode, loggerNode

DDLoggerNode *loggerNode = nil; for (DDLoggerNode *node in self._loggers) { if (node->_logger == logger) { loggerNode = node; break; }}Copy the code

If the loggerNode does not exist, the node is terminated. If yes, a willRemoveLogger notification is sent to loggerNode before the node is removed.

if ([logger respondsToSelector:@selector(willRemoveLogger)]) { dispatch_async(loggerNode->_loggerQueue, ^{ @autoreleasepool { [logger willRemoveLogger]; }}); } [self._loggers removeObject:loggerNode];Copy the code

DDLog also provides the removeAllLoggers method to zero out loggers at once, similar to lt_removeLogger:, which I won’t expand here.

Logging

Logging related methods are at the heart of DDLog, providing three types of instance methods and corresponding class methods. Let’s look at the first one:

+ (void)log:(BOOL)asynchronous
      level:(DDLogLevel)level
       flag:(DDLogFlag)flag
    context:(NSInteger)context
       file:(const char *)file
   function:(nullable const char *)function
       line:(NSUInteger)line
        tag:(nullable id)tag
     format:(NSString *)format, ... NS_FORMAT_FUNCTION(9,10);
Copy the code

If you’re familiar with this, these are the parameters that you need to construct a log message. The final variable argument in C… Args :(va_list)argList. This is the second log method. The last is for the user to provide the logMessage directly.

For… The variable parameter of c is obtained through the macro provided by C, as follows:

va_list args;
va_start(args, format);
NSString *message = [[NSString alloc] initWithFormat:format arguments:args];
va_end(args);
Copy the code

-[DDLog queueLogMessage: asynchronously:]

When the log message is ready to be distributed, the asynchronous call is made:

- (void)queueLogMessage:(DDLogMessage *)logMessage asynchronously:(BOOL)asyncFlag {
   dispatch_block_t logBlock = ^{
        dispatch_semaphore_wait(_queueSemaphore, DISPATCH_TIME_FOREVER);
        @autoreleasepool{[selflt_log:logMessage]; }};if (asyncFlag) {
        dispatch_async(_loggingQueue, logBlock);
    } else if (dispatch_get_specific(GlobalLoggingQueueIdentityKey)) {
        logBlock();
    } else {
        dispatch_sync(_loggingQueue, logBlock); }}Copy the code

Ignore logblocks and see how DDLog handles loggingQueue scheduling and how to avoid thread deadlock problems. The solution here absolutely needs to be highlighted. The main thread deadlock that you often encounter is as follows:

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@ "1");
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@ "2");
    });
    NSLog(@ "3");
}
Copy the code

This is also a case that is often asked in an interview. The core point is that the above code performs dispatch_sync on the main thread to enable synchronous waiting on the main queue. There are many solutions, such as the dispatch_MAIN_AsynC_Safe provided in SDWebImage to avoid this problem.

Going back to DDLog, now you can see why you need to queue identity one more step before dispatch_sync. In addition, this is discussed in more detail in Github issuse #812.

Next, look at logBlock, which, on the first line of code, turns semaphore_WAIT on until the number of available queues is less than maximumQueueSize. In general, we lock queueSize to ensure that the number of available queues is accurate and thread-safe. However, the author wishes that the timing of adding log mesages to the queue would be faster, since locks are more expensive.

This practice is used in many good open source libraries, such as SDWebImage.

– [DDLog lt_log:]

This method distributes the log message to any logger that satisfies it. Start by asserting QueueIdentity as usual. And then depending on the number of CPU cores is single-core or multi-core:

if (_numProcessors > 1) {  ... } else { ... }
Copy the code
  1. Multi-core processor, code as follows:
for (DDLoggerNode *loggerNode in self._loggers) {
    if (!(logMessage->_flag & loggerNode->_level)) {
        continue;
    }
    dispatch_group_async(_loggingGroup, loggerNode->_loggerQueue, ^{ @autoreleasepool {
        [loggerNode->_logger logMessage:logMessage];
    } });
}
dispatch_group_wait(_loggingGroup, DISPATCH_TIME_FOREVER);
Copy the code

A little bit about DDLog design, because a single log message can be provided to multiple types of Loggers. For example, a log may simultaneously need to be outputted to a terminal, written to a log file, and outputted to a browser via Websocket for testing purposes.

First, filter out loggerNodes whose level does not match by using logMessage->_flag. Then fetch loggerQueue and Logger from the matching loggerNode and call logMessage:.

The important thing here is that the _loggingGroup is used to associate the logMessage: with the group as a “transaction” to ensure that each LT_log: is executed sequentially. Each Logger itself is assigned a separate loggerQueue. This combination ensures concurrent logger calls and meets queueSize limits.

Another purpose of using dispatch_group_WAIT is to ensure that slow loggers are called in order, so that if there are too many tasks in the queue, the loggers may fail to complete in time and a lot of padding log messages may not be processed.

  1. On the single core processing is relatively simple, is the second step is different. No groPU operation:
dispatch_sync(loggerNode->_loggerQueue, ^{ @autoreleasepool { [loggerNode->_logger logMessage:logMessage]; }});Copy the code

Finally, after allocating logger messages, add _queueSemaphore by 1:

dispatch_semaphore_signal(_queueSemaphore);
Copy the code

lt_flush

The final DDLog method, which is triggered by a notification before the program ends, is implemented similarly to lt_log:

- (void)lt_flush { NSAssert(dispatch_get_specific(GlobalLoggingQueueIdentityKey), @"This method should only be run on the logging thread/queue"); for (DDLoggerNode *loggerNode in self._loggers) { if ([loggerNode->_logger respondsToSelector:@selector(flush)]) { dispatch_group_async(_loggingGroup, loggerNode->_loggerQueue, ^{ @autoreleasepool { [loggerNode->_logger flush]; }}); } } dispatch_group_wait(_loggingGroup, DISPATCH_TIME_FOREVER); }Copy the code

summary

DDLog is a real manager that uses semaphores and groups to efficiently schedule messages. It does the following:

  1. Manages the life cycle of loggers and notifies the addition and deletion of loggers.
  2. Generate a logMessage and, thread-safe, assign it to the corresponding Logger to process the message.
  3. Message that notifies the Logger to clear pending status immediately after the program ends.

Loggers

Now let’s talk about Logger. DDLog provides us with a logger base class DDAbstractLogger and several default implementations. Come and go;

DDAbstractLogger

AbstractLogger declarations are as follows:

@interface DDAbstractLogger : NSObject <DDLogger>
{
    @public
    id <DDLogFormatter> _logFormatter;
    dispatch_queue_t _loggerQueue;
}

@property (nonatomic, strong, nullable) id <DDLogFormatter> logFormatter;
@property (nonatomic, DISPATCH_QUEUE_REFERENCE_TYPE) dispatch_queue_t loggerQueue;
@property (nonatomic, readonly, getter=isOnGlobalLoggingQueue)  BOOL onGlobalLoggingQueue;
@property (nonatomic, readonly, getter=isOnInternalLoggerQueue) BOOL onInternalLoggerQueue;

@end
Copy the code

Let’s start with init:

Init

By default, AdstractLogger provides a total method of loggerQueue and whether the current number is loggerQueue and global loggingQueue. Initializing the loggerQueue is done in init, and that’s all init does.

const char *loggerQueueName = NULL;

if ([self respondsToSelector:@selector(loggerName)]) {
    loggerQueueName = self.loggerName.UTF8String;
}

_loggerQueue = dispatch_queue_create(loggerQueueName, NULL);
void *key = (__bridge void *)self;
void *nonNullValue = (__bridge void *)self;
dispatch_queue_set_specific(_loggerQueue, key, nonNullValue, NULL);
Copy the code

Again, get the queueName first, and here the default loggerName is NSStringFromClass([self class]); .

At the same time, the address of self is associated with the loggerQueue as a flag, and is used to determine onInternalLoggerQueue.

LogFormatter

The main thing that AdstractLogger implements is the getter/setter method for logFormatter. While the code is given a very detailed description, let’s take a look at the getter implementation.

Getter

The first is a thread-dependent assertion to ensure that the current queue is not in global queue or loggerQueue:

NSAssert(! [self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); NSAssert(! [self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax.");Copy the code

LogFormatter = loggingQueue, loggerQueue;

dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];

__block id <DDLogFormatter> result;

dispatch_sync(globalLoggingQueue, ^{
    dispatch_sync(self->_loggerQueue, ^{
        result = self->_logFormatter;
    });
});
return result;
Copy the code

Why does it take so much effort to look at a common Formatter, so much depth? Let’s look at some code:

DDLogVerbose(@"log msg 1");
DDLogVerbose(@"log msg 2");
[logger setFormatter:myFormatter];
DDLogVerbose(@"log msg 3");
Copy the code

Intuitively, the result we want to see is that the newly set formatter applies only to the third log message. However, DDLog is invoked asynchronously throughout the logging process.

  1. Log messages are ultimately executed in a separate loggerQueue, which is held by each logger;
  2. Before entering each loggerQueue, it passes through a global loggingQueue.

So, the only way to be thread-safe and intuitive is to follow the path of the log message and walk through the queue.

It should be emphasized that it is best for the Logger to access the FORMATTER VARIABLE directly internally, if necessary. Using self. May cause a thread deadlock.

Setter

Ddlog. loggingQueue -> self->_loggerQueue

@autoreleasepool {
    if (self->_logFormatter ! = logFormatter) {if ([self->_logFormatter respondsToSelector:@selector(willRemoveFromLogger:)]) {
            [self->_logFormatter willRemoveFromLogger:self];
        }

        self->_logFormatter = logFormatter;

        if ([self->_logFormatter respondsToSelector:@selector(didAddToLogger:inQueue:)]) {
            [self->_logFormatter didAddToLogger:self inQueue:self->_loggerQueue];
        } else if ([self->_logFormatter respondsToSelector:@selector(didAddToLogger:)]) {
            [self->_logFormatter didAddToLogger:self]; }}}Copy the code

DDASLLogger

ASLLogger is a wrapper around the Apple System Log API, and the NSLog we often use directs its output to two places:

  • Apple System Log
  • Standard Error (Telemetry)

ASLLogger has been deprecated in macosx 10.12 iOS 10.0 and replaced by DDOSLoger. The API behind ASLLogger is < asL.h >, which also provides several message levels

/ *! @defineblock Log Message Priority Levels Log levels of the message. */ #define ASL_LEVEL_EMERG 0 #define ASL_LEVEL_ALERT  1 #define ASL_LEVEL_CRIT 2 // DDLogFlagError #define ASL_LEVEL_ERR 3 // DDLogFlagWarning #define ASL_LEVEL_WARNING 4 //  DDLogFlagInfo, Regular NSLog's level #define ASL_LEVEL_NOTICE 5 // default #define ASL_LEVEL_INFO 6 #define ASL_LEVEL_DEBUG 7Copy the code

By default, ASL filters information above NOTICE, which is why DDLog basically has 5 log levels.

logMessage

LogMessage is how each Logger handles log messages. ASLLogger first filters the filename to DDASLLogCapture (active listening system log). Then formate the message:

NSString * message = _logFormatter ? [_logFormatter formatLogMessage:logMessage] : logMessage->_message;
Copy the code

If the message exists, an ASLMSG is generated and sent to the ASL via asL_SEND. The implementation is as follows:

const char *msg = [message UTF8String]; size_t aslLogLevel; Static char const *const level_strings[] = {"0", "1", "2", "3", "4", "5", "6", "7"}; uid_t const readUID = geteuid(); /// the effective user ID of the calling process char readUIDString[16]; /// formatted output conversion #ifndef NS_BLOCK_ASSERTIONS size_t l = (size_t)snprintf(readUIDString, sizeof(readUIDString), "%d", readUID); #else snprintf(readUIDString, sizeof(readUIDString), "%d", readUID); #endif NSAssert(l < sizeof(readUIDString), @"Formatted euid is too long."); NSAssert(aslLogLevel < (sizeof(level_strings) / sizeof(level_strings[0])), @"Unhandled ASL log level."); aslmsg m = asl_new(ASL_TYPE_MSG); if (m ! = NULL) { if (asl_set(m, ASL_KEY_LEVEL, level_strings[aslLogLevel]) == 0 && asl_set(m, ASL_KEY_MSG, msg) == 0 && asl_set(m, ASL_KEY_READ_UID, readUIDString) == 0 && asl_set(m, kDDASLKeyDDLog, kDDASLDDLogValue) == 0) { asl_send(_client, m); } asl_free(m); }Copy the code

DDOSLogger

Logging system OS_log is a new generation logging system. It replaced the ASL manual as follows:

The unified logging system provides a single, efficient, high performance set of APIs for capturing log messages across all levels of the system. This unified system centralizes the storage of log data in memory and in a data store on disk.

It provides a centralized storage of log records. The API is also very clean, and there are opportunities to expand on OS_Log.

Init

First, the OSLogger needs to hold a log object:

os_log_t os_log_create(const char *subsystem, const char *category);
Copy the code

subsystem

An identifier string, in reverse DNS notation, that represents the subsystem that’s performing logging, for example, com.your_company.your_subsystem_name. The subsystem is used for categorization and filtering of related log messages, as well as for grouping related logging settings.

category

A category within the specified subsystem. The system uses the category to categorize and filter related log messages, as well as to group related logging settings within the subsystem’s settings. A category’s logging settings override those of the parent subsystem.

By the way, the official OS_log documentation is only provided with the Swift description, oslog.category details here.

LogMessage

Also filter filename to DDASLLogCapture log message and to log message formatter. The OS_LOG API is friendly and concise. Each OS_LOG_type_t API provides the following methods:

__auto_type logger = [self logger];
switch (logMessage->_flag) {
    case DDLogFlagError  :
        os_log_error(logger, "%{public}s", msg);
        break;
    case DDLogFlagWarning:
    case DDLogFlagInfo   :
        os_log_info(logger, "%{public}s", msg);
        break;
    case DDLogFlagDebug  :
    case DDLogFlagVerbose:
    default              :
        os_log_debug(logger, "%{public}s", msg);
        break;
}
Copy the code

DDTTYLogger

This class provides a logger for Terminal output or Xcode console output, depending on where you are running your code.

It directs logs to terminals and Xcode terminals with color support. Xcode support requires adding the XcodeColors plug-in. There are thousands of lines of code inside TTYLogger. But it’s a simple thing to do. Based on the color range supported by different terminal types, the set color is adapted to the final output.

There are three main types of color range:

  • Standard Shell: Supports only 16 colors
  • Terminal.app: Supports 256 colors
  • xterm colors

See ANSI_escape_code for details.

LogMessage

TTYLogger supports configuring a different color for each logFlag, and then encapsulates the color and flag in the DDTTYLoggerColorProfile class, stored in _colorProfilesDict. LogMessage consists of three main steps:

  1. throughlogMessage->_tagRemove colorProfile that;
  2. Convert log message to C string;
  3. To write color intoiovec v[iovec_len], the final callwritev(STDERR_FILENO, v, iovec_len);The output.

To be continued

The above three loggers are basic terminal outputs and can be used as an alternative to NSLog. DDFileLogger, DDAbstractDatabaseLogger, and various extensions such as WebSocketLogger are not included in this article due to space limitations. There is also a whole section of Formatters to be put down in one article.

In this paper, the author of Lumberjack makes full use of the features of GCD to achieve secure and efficient asynchronous logging by using DDLog class for GCD. The entire process does not use locks to address thread-safety, which is good GCD practice. The author also produced CocoaAsyncSocket, XMPPFramework, CocoaHTTPServer and other well-known libraries. After that, you can slowly refine.

Finally, paste a collated brain map, relatively simple, do not like spray.