Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How should I handle logs in an Objective-C library?

I’m writing an Objective-C library and in some places I’d like to log some information. Using NSLog is not ideal since it’s not configurable and has neither level support nor tag support. CocoaLumberjack and NSLogger are both popular logging libraries supporting levels and contexts/tags but I’d prefer not to depend on a third party logging library.

How can I produce logs in a configurable way that doesn’t force a specific logging library upon my users?

like image 971
0xced Avatar asked Jan 11 '16 23:01

0xced


People also ask

What are logging libraries?

A logging library (or logging framework) is code that you embed into your application to create and manage log events. Logging libraries provide APIs for creating, structuring, formatting, and transmitting log events in a consistent way. Like agents, they're used to send events from your application to a destination.

What is the difference between NSLog and printf?

NSLog is like a printf, but it does a bit more: A timestamp is added to the output. The output is sent to the Xcode console, or whatever stderr is defined as. It accepts all the printf specifiers, but it also accepts the @ operator for objects which displays the string provided by the object's description method.

Where does NSLog write to?

NSLog outputs messages to the Apple System Log facility or to the Console app (usually prefixed with the time and the process id). Many of the system frameworks use NSLog for logging exceptions and errors, but there is no requirement to restrict its usage to those purposes.


1 Answers

TL;DR Expose a log handler block in your API.


Here is a suggestion to make logging configurable very easily with a logger class as part of your public API. Let’s call it MYLibraryLogger:

// MYLibraryLogger.h

#import <Foundation/Foundation.h>

typedef NS_ENUM(NSUInteger, MYLogLevel) {
    MYLogLevelError   = 0,
    MYLogLevelWarning = 1,
    MYLogLevelInfo    = 2,
    MYLogLevelDebug   = 3,
    MYLogLevelVerbose = 4,
};

@interface MYLibraryLogger : NSObject

+ (void) setLogHandler:(void (^)(NSString * (^message)(void), MYLogLevel level, const char *file, const char *function, NSUInteger line))logHandler;

@end

This class has a single method that allows client to register a log handler block. This makes it trivial for a client to implement logging with their favorite library. Here is how a client would use it with NSLogger:

[MYLibraryLogger setLogHandler:^(NSString * (^message)(void), MYLogLevel level, const char *file, const char *function, NSUInteger line) {
    LogMessageRawF(file, (int)line, function, @"MYLibrary", (int)level, message());
}];

or with CocoaLumberjack:

[MYLibraryLogger setLogHandler:^(NSString * (^message)(void), MYLogLevel level, const char *file, const char *function, NSUInteger line) {
    // The `MYLogLevel` enum matches the `DDLogFlag` options from DDLog.h when shifted
    [DDLog log:YES message:message() level:ddLogLevel flag:(1 << level) context:MYLibraryLumberjackContext file:file function:function line:line tag:nil];
}];

Here is an implementation of MYLibraryLogger with a default log handler that only logs errors and warnings:

// MYLibraryLogger.m

#import "MYLibraryLogger.h"

static void (^LogHandler)(NSString * (^)(void), MYLogLevel, const char *, const char *, NSUInteger) = ^(NSString *(^message)(void), MYLogLevel level, const char *file, const char *function, NSUInteger line)
{
    if (level == MYLogLevelError || level == MYLogLevelWarning)
        NSLog(@"[MYLibrary] %@", message());
};

@implementation MYLibraryLogger

+ (void) setLogHandler:(void (^)(NSString * (^message)(void), MYLogLevel level, const char *file, const char *function, NSUInteger line))logHandler
{
    LogHandler = logHandler;
}

+ (void) logMessage:(NSString * (^)(void))message level:(MYLogLevel)level file:(const char *)file function:(const char *)function line:(NSUInteger)line
{
    if (LogHandler)
        LogHandler(message, level, file, function, line);
}

@end

The last missing piece for this solution to work is a set of macros for you to use through your library.

// MYLibraryLogger+Private.h

#import <Foundation/Foundation.h>

#import "MYLibraryLogger.h"

@interface MYLibraryLogger ()

+ (void) logMessage:(NSString * (^)(void))message level:(MYLogLevel)level file:(const char *)file function:(const char *)function line:(NSUInteger)line;

@end

#define MYLibraryLog(_level, _message) [MYLibraryLogger logMessage:(_message) level:(_level) file:__FILE__ function:__PRETTY_FUNCTION__ line:__LINE__]

#define MYLibraryLogError(format, ...)   MYLibraryLog(MYLogLevelError,   (^{ return [NSString stringWithFormat:(format), ##__VA_ARGS__]; }))
#define MYLibraryLogWarning(format, ...) MYLibraryLog(MYLogLevelWarning, (^{ return [NSString stringWithFormat:(format), ##__VA_ARGS__]; }))
#define MYLibraryLogInfo(format, ...)    MYLibraryLog(MYLogLevelInfo,    (^{ return [NSString stringWithFormat:(format), ##__VA_ARGS__]; }))
#define MYLibraryLogDebug(format, ...)   MYLibraryLog(MYLogLevelDebug,   (^{ return [NSString stringWithFormat:(format), ##__VA_ARGS__]; }))
#define MYLibraryLogVerbose(format, ...) MYLibraryLog(MYLogLevelVerbose, (^{ return [NSString stringWithFormat:(format), ##__VA_ARGS__]; }))

Then you just use it like this inside your library:

MYLibraryLogError(@"Operation finished with error: %@", error);

Notice how the log message is a block returning a string instead of just a string. This way you can potentially avoid expensive computations if the defined log handler decides not to evaluate the message (e.g. based on the log level as in the default log handler above). This lets you write one-liner logs with potentially costly log messages to compute with no performance hit if the log is discarded, for example:

MYLibraryLogDebug(@"Object: %@", ^{ return object.debugDescription; }());
like image 146
0xced Avatar answered Nov 15 '22 12:11

0xced