When working as part of a team, different developers are working on different parts of the application. You may be adding a new feature to FileA, while your colleague is trying to track down a bug in FileB. During your development and testing, you increase the log level of FileA to verbose. This spits out a bunch of useful log statements so you can ensure that new feature is working exactly as intended. Excited to see it working, you quickly commit and push your changes. But you forgot to decrease the log level of FileA, and now your colleague sees a bunch of new log statements that don’t apply to his/her current development work…

It’s a typical situation. A bit annoying, with several unnecessary commits going back and forth, changing the log levels.

A better solution is using per-user log levels:

// Log levels: off, error, warn, info, verbose
#if defined(DEBUG) && defined(robbie_hanson)
  static const DDLogLevel ddLogLevel = DDLogLevelVerbose; // Log level for robbie (debug)
#elif defined(DEBUG)
  static const DDLogLevel ddLogLevel = DDLogLevelInfo;    // Log level for other team members (debug)
#else
  static const DDLogLevel ddLogLevel = DDLogLevelWarn;    // Log level for release build
#endif

(Different log levels per debug / release builds are explained in Xcode Tricks)

This way, the developer who’s currently working on the file can have their own log level. And everyone else just gets the normal log level. The trick is defining that full_username preprocessor macro.

An example of how to do this is included in the repository under the Xcode/PerUserLogLevels project. Which I’ll explain here.

The first step is adding a new header file to your project. We’ll call it LumberjackUser.h. You can leave the file completely empty. But when it’s all said and done, the file will end up looking like this:

// This file is automatically generated
#define robbie_hanson 1

Of course, when you compile the project on your computer, robbie_hanson will get replaced with whatever_your_full_username_is.

Note: You should not add LumberjackUser.h to source control. But it does need to be added to the Xcode project.

Next we’re going to add a script to our project that will automatically generate/update the LumberjackUser.h file. I placed the script in a file named LumberjackUser.bash:

#!/bin/bash

# Get full user name of current user
# E.g. "Robbie Hanson"
full1=$(osascript -e "tell application \\"System Events\\"" -e "get the full name of the current user" -e "end tell")
#echo $full1

# Convert to lower case
# E.g. "robbie hanson"
full2=$(echo $full1 | awk '{print tolower($0)}')
#echo $full2

# Replace spaces with underscores
# E.g. "robbie_hanson"
full3=$(echo ${full2// /_})
#echo $full3

# Remove any characters that are illegal in a macro name
full4=$(echo $full3 | sed 's/[^0-9a-zA-Z_]*//g')
#echo $full4

# If we output directly to our intended file, even when nothing has changed,
# then we'll essentially be doing a touch on the file.
# The compiler will see this, and recompile any files that include the header.
# This may mean recompiling every single source file, every single time we do a build!
# So instead we're going to output to a temporary file, and use diff to detect changes.

temp_filepath="${SRCROOT}/PerUserLogLevels/LumberjackUser.temp.h"
final_filepath="${SRCROOT}/PerUserLogLevels/LumberjackUser.h"

echo "// This file is automatically generated" > ${temp_filepath}
echo "#define $full4 1" >> ${temp_filepath}

if [ -a ${final_filepath} ]
    then
    DIFF=$(diff ${temp_filepath} ${final_filepath}) 
    if [ "$DIFF" != "" ] 
    then
        cp -f ${temp_filepath} ${final_filepath}
    fi
else
    cp -f ${temp_filepath} ${final_filepath}
fi

Next we’re going to add a new “Run Script” Build Phase to our target, ensure it’s run before the Compile Sources phase, and have it execute our bash script from above:

Screenshot of Xcode

Finally, you’ll want to add LumberjackUser.h and LumberjackUser.temp.h to .gitignore

And that’s all there is to it. Happy Logging.