It’s easy to understand why your mobile application users are frustrated with crashes and known bugs. However, it is less easy to understand why users are frustrated with the app when it doesn’t crash. Many things can go wrong in your application flow: business logic may be improperly implemented, and users may enter unexpected input or otherwise interact with the app in unexpected ways. Analytics can help uncover some basic issues, but nothing beats a descriptive log.
Log Everything That Matters
If your user or application has to make a decision, the decision should be logged. Every time an unexpected behavior is encountered, the behavior should be logged. These types of conditional cases should be described in enough detail that a developer can easily follow the user’s path through your code. Here are some decisions, behaviors, and data that should be in your log:
- – The reason an “if” block is being executed.
- – The reason an “else” block is being executed. Also, always have an “else” block!
- – Any user-inputted data that was entered immediately before catching an exception.
- – Any request data, including headers and request body, sent for a failed web request.
- – Any response data, including headers and response body, that can not be processed.
- – File paths for any files that are created, read, updated, or deleted.
- – The presence of device sensors (e.g., an NFC radio) required for a feature to work.
- – The availability of network connection paths like WiFi and 4G data.
- – Usernames, record IDs, and any other information relevant to what is happening.
- – Any exception, using appropriate error-level logging to include stacktraces.
Log Things Consistently
A standard format should be specified and followed for your log messages. Here are some examples of well-formed log messages:
[2014-10-12 04:15:01 PM][HomeScreen][INFO] User opened the AccountsScreen.[2014-10-12 04:15:02 PM][AccountsScreen][INFO] User clicked the Add button.[2014-10-12 04:16:00 PM][AccountsService][INFO] User added Account[dave].[2014-10-12 04:16:04 PM][HomeScreen][INFO] User opened the FriendsScreen.[2014-10-12 04:16:07 PM][ProfileService][INFO] Account[dave] requesting friends list.[2014-10-12 04:16:08 PM][ProfileService][WARN] Account[dave] has no friends.[2014-10-12 04:16:08 PM][FriendsScreen][WARN] App notified Account[dave] to add friends.
The above log statements take the following form:
[TIMESTAMP][TAG][LEVEL] ACTOR ACTION DIRECT_OBJECT
A well-structured log message format enables us to quickly find information in the log without necessarily knowing the exact statement. For instance, if you wanted to find out what account the user added, you could use a text processing tool like grep to find log messages based upon regular expressions:
$ grep “.* Account[dave]” application.log
Regardless of your chosen format, your log messages should be consistent. A good log message contains the following details:
- Timestamp
- Name of the class or component that is writing the log statement
- The actor performing the action (e.g., the user, the application, the OS, etc.)
- The action being performed
- The thing the action is being performed on
- Any relevant database record IDs, usernames, and non-critical tokens (no passwords!)
Debug-Level and Verbose Logging
Oftentimes, developers will find it important to log variable values while debugging specific issues. More often than not, a developer building a temporary log statement will provide no context around the values being output to the log, nor will they remember to remove all of these debug-level log statements.
Ask yourself if your debug-level logging should be persistent. If not, perhaps consider using breakpoints set by a debugger to determine variable values during debugging. If the log messages will be useful in the future, however, make them more informative. Debug-level log messages may not share the consistent format described earlier for info-level log messages, but they should still be descriptive of the state of the application and provide context into the meaning of values printed. Simply logging the number “42” gives no context to your future self, whereas logging “Number of friends: 42” will.
Logging Performance
There can be a performance drawback to debug-level logging. String concatenation, string formatting, and retrieving the current timestamp can all take time. In performance-focused areas of code, the time spent performing these seemingly simple operations can result in noticeable slow-downs. Most logging frameworks such as log4j, slf4j, or the Rails Logger, will have ways to check the current log level (a comparatively quick operations) before executing a log statement line. For example:
if logger.debug? logger.debug “This only prints if the current log level allows debug-level logging.”end
Use a Logging Service to Collect Mobile App Logs
Once you have useful logs, you will need some way to collect them. Crashlytics offers a dead-simple logging service for Android and iOS applications. You may need to get creative if working on other mobile platforms. For example, Metova build a proprietary logging and reporting server to collect BlackBerry application logs because no service existed for BlackBerry.
Your Server Logs Are Important, Too
Logging what happens on a mobile device will get you far in debugging production app problems. Sometimes, though, you will track problems down to your back-end server: API responses may contain unexpected data, web requests may fail, and data you assume is on the server may simply not exist. All modern web application frameworks support logging, and collecting web app logs is easier than retrieving mobile app logs from your users: the web app logs simply exist on your server. There are also centralized logging services for web app servers that can allow you to share logs with mobile developers, such as logentries, Loggly, and AWS CloudWatch Logs.
Examining both mobile app logs and web app logs can give you a clear picture of what is happening when your users encounter problems.