Reputation: 26583
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?
Upvotes: 12
Views: 2413
Reputation: 26583
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; }());
Upvotes: 22