Skip to content

Latest commit

 

History

History
258 lines (192 loc) · 10.7 KB

Advanced_Logging.md

File metadata and controls

258 lines (192 loc) · 10.7 KB

Advanced Logging

This is about

Log Levels

NXLogging supports the eight log levels first introduced with syslog back in the 1980s. The Apple System Log (ASL) uses the same:

  • Debug
  • Info
  • Notice
  • Warning
  • Error
  • Critical
  • Alert
  • Emergency

In your code you will use these log levels with the prefix NXLogLevel. In Objective C you could write

NXLogLevel level = NXLogLevelWarning;

and in Swift

let level: NXLogLevel = NXLogLevel.Warning // or for short: .Warning

On a side note: In our documentation ---and partially in the API--- we will use the terms log level and severity, which refer to the same concept but counter-directional: Debug is the highest log level (most detailled) but has the lowest severity (least impact). Therefore, if you configure a log target's maximum log level to Error, it will log only log messages with a level up to Error (Error, Critical, Alert and Emergency). For adjusting the maximum log level, refer to Customisation.

The log levels are defined in the public header file NXLogTypes.h. You will also find some additional documentation there.

You can use the log levels to your liking but know, that some backends might filter log messages according to their severity. The Apple System Log, e.g. by default only accepts messages with a severity of Notice or higher.

The standard log configured in NXLogging logs to the console (usually the Xcode debug console) and to the system (ASL, usually what you see accessing the system) log. The maximum log level for the console is preconfigured to Debug and for the system to Notice (the latter coinsides with the default setting of ASL).

Logging errors

Here is an example on error handling and logging in Objective C:

NSString *path = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"txt"];
NSError *error = nil;
NSString *content = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error];

if(error) {
    NXLogError(NXLogLevelError, error, @"Unable to read file %@", path.lastPathComponent);
} else {
    NXLog(NXLogLevelDebug, @"Got content:'%@' from file %@", content, path.lastPathComponent);
}

and here in Swift:

let path : NSString! = NSBundle.mainBundle().pathForResource("test", ofType: "txt")

do {
    let content = try NSString(contentsOfFile: path as String, encoding: NSUTF8StringEncoding)
    NXLogger.log(.Debug, format: "Got content '%@' from file %@", content, path.lastPathComponent)
} catch {
    NXLogger.log(.Error, error: error, format: "Unable to read file %@", path.lastPathComponent)
}

Appart from the obviously different error handling in the two languages, the logging is pretty much the same. If the encoding file in the examples is not UTF-8, you'll get a log message like this:

2016-03-12 16:37:43 <Error>: com.naxos-software.NXLoggingSample logSomething()(LogClientSwift.swift:92) - Unable to read file test.txt
   Error: The operation couldn’t be completed. (NSCocoaErrorDomain, error 261)

Here's another Swift example. Assume the implementation of a vending machine (adopted from Apple's error handling guide):

enum VendingMachineError: ErrorType {
    case InvalidSelection
    case InsufficientFunds(coinsNeeded: Int, coinsInserted: Int)
    case OutOfStock
}

func buyBeer(coins: Int) throws {
    if (coins < 5) {
        throw VendingMachineError.InsufficientFunds(coinsNeeded: 5, coinsInserted: coins)
    } else {
        ejectBeer();
    }
}

As a caller, you can simply log an error like this:

do {
    try buyBeer(3)
} catch {
    NXLogger.log(error: error)
}

In Swift, you can omit the log level for the log method and leave it to the logger to find a suitable one. Here it will use NXLogLevel.Error, because an ErrorType was logged. Also note the details of the error message, which you get in the second line of the log output even though the error was just an enum value:

2016-03-12 16:37:43 <Error>: com.naxos-software.NXLoggingSample swiftSpecificExamples()(LogClientSwift.swift:130)
   Error: InsufficientFunds(5, 3) (NXLoggingSample.VendingMachineError, error 1)

You can also handle the error in a slightly more elaborate way and add some information to the log by using the NSError extension of NXLogging:

do {
    try buyBeer(3)
} catch VendingMachineError.InsufficientFunds(let p) {
    NXLogger.log(error: NSError(VendingMachineError.InsufficientFunds(p),
        reason: "\(p.coinsInserted) coins inserted but \(p.coinsNeeded) coins are needed",
        suggestion: "Insert \(p.coinsNeeded - p.coinsInserted) more coins"))
} catch let error as VendingMachineError {
    NXLogger.log(error: NSError(error, suggestion: "Try banging the machine"))
} catch {
    NXLogger.log(error: error)
}

The result will look something like this:

2016-03-12 16:37:43 <Error>: com.naxos-software.NXLoggingSample swiftSpecificExamples()(LogClientSwift.swift:145)
   Error: InsufficientFunds(5, 3) (NXLoggingSample.VendingMachineError, error 1)
      Reason: 3 coins inserted but 5 coins are needed
      Suggestion: Insert 2 more coins

Especially in Objective C, where you are stuck with NSError, NXLogging's extension makes it more convenient creating NSError instances:

NSError *err = [NSError errorWithCode: -2
                          description: @"Unable to complete this operation"
                               reason: @"The request timed out"
                           suggestion: @"Try again later"
                      underlyingError: [NSError errorWithCode: -1 description: @"Time-out error"]];

In most cases you don't have to deal with the userInfo dictionary and if omitted, NSError will use an error domain derived from the bundle identifier of your application. Logging the error above will result in log output similar to the one below (note how underlying errors are included recursively):

2016-03-12 16:37:43 <Error>: com.naxos-software.NXLoggingSample [LogClientObjC logSomething](LogClientObjC.m:33)
   Error: Unable to complete this operation (NXLoggingSampleErrorDomain, error -2)
      Reason: The request timed out
      Suggestion: Try again later
   Caused by: Time-out error (NXLoggingSampleErrorDomain, error -1)

Logging exceptions

We don't want to encourage you to raise and catch exceptions all over your Objective-C or (even worse) Swift code. In rare cases however, exception handling might be necessary. NXLogging comes with an extension to the NSException class, which allows for nested exceptions by adding the property cause. It also adds a static method probe, which is particular useful in Swift, because Swift does not come with a standard way to catch a raised NSException.

In Objective C you can create and raise and catch exceptions like this:

- (void)badGuy {
    [NSException raise:@"BadGuyException" format:@"Trouble is my middle name"];
}

- (void)handleBadGuy {
    NSException *exception = [NSException probe:^{ [self badGuy]; }];

    [NSException raise:@"TooBadException" cause:exception format:@"Can't handle the %@", @"bad guy"];
}

And the equivalent in Swift:

func badGuy() {
    NSException.raise("BadGuyException", format: "Trouble is my middle name")
}

func handleBadGuy() {
    let exception = NSException.probe(badGuy)

    NSException.raise("TooBadException", cause: exception, format: "Can't handle the %@", "bad guy")
}

Now, if you log an exception in Objective C:

NSException *exception = [NSException probe:^{ [self handleBadGuy]; }]; // We could also do @try @catch here

NXLogException(NXLogLevelNotice, exception, @"Something sinister is going on");

or in Swift:

let exception = NSException.probe(handleBadGuy)
    
NXLogger.log(.Notice, exception: exception, format: "Something sinister is going on")

you'll end up with log like this:

2016-03-12 16:37:43 <Notice>: com.naxos-software.NXLoggingSample logSomething()(LogClientSwift.swift:90) - Something sinister is going on
   Exception: Can't handle the bad guy (TooBadException)
   Caused by: Trouble is my middle name (BadGuyException)
      >> Log with severity Error or higher to enable call stack symbols <<

As the last line of the log output states, logging an exception with the severity Error or above, will cause the logger to include the call stack symbols in the log. You can configure this threshold on the log target (see Customisation).

Log variables

Let us assume, that you don't want every line in your logs to include a lot of information on the client's environment, which sometimes might be advisable for a production system (see Customisation on how to achieve this). Let us also assume, that for some particular log messages you do want to include some info. In such a case, you can make use of log variables:

NXLogger.log(.Warning, format: "This feature is not available in $(systemName) $(systemVersion).")

NXLogger.log(.Error, format: "Sorry, this device ($(deviceModel)) does not make coffee.")

// You can also put variables in the format arguments ...
NXLogger.log(.Info, format: "Application %@ started on %@ with process ID %@.", "$(processName)", "$(deviceName)", "$(processID)")

// ... or even mix it like this
NXLogger.log(.Emergency, format: "HELP WANTED: Can some Swift expert fix function $(%@) in file $(%@)?", "function", "file")

The variables currently supported are:

  • file
  • function
  • line
  • module
  • date
  • processName
  • processID
  • deviceName
  • deviceModel
  • systemName
  • systemVersion

Logging with the statements above, will result in log lines like this:

2016-03-12 16:37:43 <Warning>: This feature is not available in iPhone OS 9.2.
2016-03-12 16:37:43 <Error>: Sorry, this device (x86_64) does not make coffee.
2016-03-12 16:37:43 <Info>: Application NXLoggingSample started on iPhone Simulator with process ID 63132.
2016-03-12 16:37:43 <Emergency>: HELP WANTED: Can some Swift expert fix function logSomething() in file LogClientSwift.swift?