Friday, February 6, 2015

Logging

My first development blog post, and what other topic would I pick other than the boring topic of Logging!  It might be boring, but it's fundamental in all we devs do.  I don't know about you, but I hate bringing in big frameworks or complex code, especially just to do logging.  I prefer to have my own simple set of classes, and if I want to later, I can simply pipe the results to an existing framework depending on the project I'm working on.

So here we go, how do we create a simple logging capability that is flexible enough to plug in else where?  It's simple!  I'll show you just what I did, in this case in Objective-C, but the concepts can easily convert to any other syntax.

So let's start with the basics, logging levels.  These are pretty standard across the board, and start from most amount of detail, to least.  So for example, selecting logging level "info" will include info message, as well as debug and fine messages.  So let's define an enum that represents this (note, using MDB as the prefix which stands for my blog name "My Dev Bits"):

enum MDBLogLevel {
    MDBLogLevelFine = 1,
    MDBLogLevelDebug,
    MDBLogLevelInfo,
    MDBLogLevelWarn,
    MDBLogLevelError
};

Next, let's define the how we get a Log we can use in our code.  We'll create two static methods:

+ (MDBLog *)logWithClass:(Class)logClass;
+ (MDBLog *)logWithContext:(NSString *)logContext;

The first will create a "Context" (a simple string) using the name of the Class.  If you want to use something else, you can specify that in the second method.

Now, we will want to be able to configure the Log globally, so we will add a static method for that.

+ (MDBLog *)configuration;

And a static helper method that we can use later to format our Log strings.

+ (NSString *)nameOfLevel:(int)logLevel;

Now we need to define a set of properties and getters/setters for a Log instance.  In this case, a Log instance will have a 1) logging level (the level at which it will either output or suppress the log output), 2) the format of the logging string, and 3) a string log context. 

@property (readwrite, nonatomic) enum MDBLogLevel logLevel;
@property (readonly, nonatomic, copy) NSString *logFormat;
@property (readonlynonatomiccopyNSString *logContext;

The log Level and log Format could actually just be statically defined, but instead I made them instance variables to give the most flexibility in case there is a reason to change it for a particular logging instance.  But we don't want to do this all the time, so in order to have default values, we will create a "prototype" Log instance that we will create internally and will be used to set the defaults for any new Log instances created.  More on that later.

Now we need to define the methods a client can use to instruct the Log to output some information, let's define them this way to match the possible logging levels:

// Determines if the Log is outputting at the specified intent level.
- (BOOL)levelEnabled:(enum MDBLogLevel)intentLevel;

// Determines if the particular Log level is enabled.
- (BOOL)fineEnabled;
- (BOOL)debugEnabled;
- (BOOL)infoEnabled;
- (BOOL)warnEnabled;
- (BOOL)errorEnabled;

- (void)log:(enum MDBLogLevel)logLevel method:(SEL)method format:(NSString *)format, ...;

- (void)fine:(SEL)method format:(NSString *)format, ...;
- (void)debug:(SEL)method format:(NSString *)format, ...;
- (void)info:(SEL)method format:(NSString *)format, ...;
- (void)warn:(SEL)method format:(NSString *)format, ...;
- (void)error:(SEL)method format:(NSString *)format, ...;

The main logging method here is the "log" method and it takes a logging level, a method that is calling the log, and the string that defines the format of this particular logging message.

Also note that the last parameter is "..." and the refers to a dynamic list of parameters that an be provided.  This will allow the client to call the logging methods and pass a string that will have substitute values, and then provide the list of those values to substitute.

The other methods are for convenience and correspond to logging at a particular logging level and will ultimately just call the more generic "log" method specifying the log level.

By default, we will just write the logging output to the console.  But we want the ability to "pipe" the logging output to some other output (e.g. another logging facility perhaps).   We can accomplish this simply by defining an interface for a "log writer" and then let clients of this Log facility create their own and plug them in.  So let's define that, first the interface:

@protocol MDBLogWriter <NSObject>

- (void)log:(MDBLog *)log logLevel:(enum MDBLogLevel)logLevel method:(SEL)method 
format:(NSString *)format args:(va_list)args;

@end

Implementors of this interface will get the calling Log class, the log level to output at, the calling method, a format for the string, and the variable list of argument parameters to replace in the format string.

We will also create a default Log Writer that will just output to the console, and we can define that with a property for the Log Writer in a particular Log instance.

@property (readwrite, nonatomic) id<MDBLogWriter> logWriter;

And a static method to get the default log writer:

+ (id<MDBLogWriter>)defaultLogWriter;

And then define the default Log Writer instance:

@interface MDBLogWriterDefault : NSObject <MDBLogWriter>
@end 

So that's it, this defines our Log class and related facilities.  In my next blog post I'll describe what these properties and methods do, and how to implement them.  In the mean time, read more here to get the complete header file...

MDBLog.h

@class MDBLog;

enum MDBLogLevel {
    MDBLogLevelFine = 1,
    MDBLogLevelDebug,
    MDBLogLevelInfo,
    MDBLogLevelWarn,
    MDBLogLevelError
};

@protocol MDBLogWriter <NSObject>

- (void)log:(MDBLog *)log logLevel:(enum MDBLogLevel)logLevel method:(SEL)method format:(NSString *)format args:(va_list)args;

@end

@interface MDBLog : NSObject

@property (readwrite, nonatomic) enum MDBLogLevel logLevel;
@property (readonly, nonatomic, copy) NSString *logContext;
@property (readonly, nonatomic, copy) NSString *logFormat;

@property (readwrite, nonatomic) id<MDBLogWriter> logWriter;

+ (MDBLog *)configuration;
+ (MDBLog *)logWithClass:(Class)logClass;
+ (MDBLog *)logWithContext:(NSString *)logContext;
+ (NSString *)nameOfLevel:(int)logLevel;

+ (id<MDBLogWriter>)defaultLogWriter;

- (BOOL)levelEnabled:(enum MDBLogLevel)intentLevel;

- (BOOL)fineEnabled;
- (BOOL)debugEnabled;
- (BOOL)infoEnabled;
- (BOOL)warnEnabled;
- (BOOL)errorEnabled;

- (void)log:(enum MDBLogLevel)logLevel method:(SEL)method format:(NSString *)format, ...;

- (void)fine:(SEL)method format:(NSString *)format, ...;
- (void)debug:(SEL)method format:(NSString *)format, ...;
- (void)info:(SEL)method format:(NSString *)format, ...;
- (void)warn:(SEL)method format:(NSString *)format, ...;
- (void)error:(SEL)method format:(NSString *)format, ...;

@end

@interface MDBLogWriterDefault : NSObject <MDBLogWriter>

@end

No comments:

Post a Comment