Everyone knows that print
is probably the most ubiquitous and helpful debugging software in a developer’s toolbox. Positive, we have now breakpoints too however what’s the enjoyable in that? Sprinkling some prints all through our codebase to debug an issue is far more enjoyable! And naturally once we print greater than we will deal with we simply add some helpful prefixes to our messages and we’re good to go once more.
What if i advised that you are able to do manner higher with just some strains of code. You possibly can ship your prints to extra locations, give them a precedence, and extra. After all, we don’t name it printing anymore; we name it logging.
Logging is a key technique to amassing essential knowledge on your app. From easy debugging strings to recording whole chains of occasions, having an excellent logging technique will help you debug issues when you’re writing your app in Xcode and in addition when you’ve shipped your app to the shop.
On this put up, I’d like to point out you how one can arrange a Logger
from the OSLog
framework in your app, and the way you need to use it to log messages that may assist you to debug your app and acquire insights about issues your customers expertise.
Establishing a Logger object
To arrange a logger object all that you must do is import OSLog
and create an occasion of the Logger
object:
import OSLog
let logger = Logger()
struct MyApp: App {
// ...
}
This method creates a world logger object that you need to use from wherever inside your app. Since I didn’t cross any customized configuration, the logger will simply log messages utilizing the default parameters.
That mentioned, it’s clever to truly present two items of configuration on your logger:
By offering these two parameters, you may make filtering log messages quite a bit simpler, and it permits you to group messages from a number of loggers collectively.
For instance, I prefer to create an information mannequin debugger that I can use to log knowledge mannequin associated data. Right here’s how I can create such a logger:
let modelLogger = Logger.init(
subsystem: "com.myapp.fashions",
class: "myapp.debugging"
)
Apple recommends that we identify our subsystems utilizing reverse-DNS notation. So for instance, com.myapp.fashions
for a subsystem that encompasses fashions inside my app. You may create loggers for each module in your app and provides every module its personal subsystem for instance. That manner, you’ll be able to simply determine which module generated which log messages.
The second argument offered to my logger is a class
. I can use this class to group associated messaged collectively, even once they originated from completely different subsystems. Apple doesn’t present any naming conventions for class
so you are able to do no matter you need right here.
It’s completely acceptable for a single app to have a number of loggers. You possibly can create a number of loggers for a single subsystem for instance to be able to present completely different classes. Having narrowly scoped loggers in your apps with well-named classes and subsystems will vastly enhance your debugging expertise as we’ll see in a while.
When you’ve created an occasion of your logger and located a pleasant place to carry on to it (I often prefer to have it accessible as a world fixed however you may wish to inject it or wrap it in a category of your individual) you can begin sending your first log messages. Let’s see how that works.
Logging your first messages
Once you log messages by your logger occasion, these messages will find yourself somewhere else relying on which sort of log stage you’re utilizing. We’ll focus on log ranges later so for now we’ll simply use the easy log
technique to log our messages.
Let’s log a easy “Good day, world!” message in response to a button faucet in SwiftUI:
Button("Good day, world") {
modelLogger.log("Good day, world!")
}
Calling log
in your Logging
occasion will trigger a message to be printed in your Xcode console, similar to it might with print…
Nonetheless, as a result of we’re utilizing a Logger
, we will get Xcode to point out us extra data.
Right here’s an instance of the varieties of knowledge you’ll be able to view in your console.
Personally, I discover the timestamp to be probably the most attention-grabbing facet of this. Usually your print statements gained’t present them and it may be laborious to tell apart between issues that occurred a second or two aside and issues that occur concurrently or in very speedy succession.
For comparability, right here’s what the identical string appears like once we print it utilizing print
There’s no further data so we have now no clue of when precisely this assertion was printed, by which subsystem, and how much debugging we have been making an attempt to do.
Xcode gained’t present you all the knowledge above by default although. You might want to allow it by the metadata menu within the console space. The great factor is, you don’t have to have achieved this earlier than you began debugging so you’ll be able to allow that everytime you’d like.
Gaining a lot perception into the knowledge we’re logging is tremendous precious and may actually make debugging a lot simpler. Particularly with logging classes and subsystems it’ll be a lot simpler to retrace the place a log message got here from with out resorting to including prefixes or emoji to your log messages.
If you wish to filter all of your log messages by subsystem or class, you’ll be able to truly simply seek for your log message utilizing the console’s search space.
Discover how Xcode detects that I’m trying to find a string that matches a identified subsystem and it gives to both embody or exclude subsystems matching a given string.
This lets you simply drown out all of your logging noise and see precisely what you’re concerned with. You possibly can have as many subsystems, classes, and loggers as you’d like in your app so I extremely suggest to create loggers which are used for particular functions and modules in the event you can. It’ll make debugging a lot simpler.
Accessing logs exterior of Xcode
There are a number of methods so that you can acquire entry to log messages even when Xcode isn’t operating. My private favourite is to make use of Console app.
Discovering logs within the Console app
By means of the Console app in your mac you’ll be able to hook up with your telephone and see a reside feed of all log messages which are being despatched to the console. That features messages that you just’re sending from your individual apps, as you’ll be able to see right here:
The console gives loads of filtering choices to ensure you solely see logs which are attention-grabbing to you. I’ve discovered the Console app logging to be invaluable whereas testing stuff that entails background up- and downloads the place I might shut my app, drive it out of reminiscence (and detach the debugger) so I may see whether or not all delegate strategies are referred to as on the proper instances with the anticipated values.
It’s additionally fairly helpful to have the ability to plug in a telephone to your Mac, open Console, and browse your app’s logs. Inside an workplace this has allowed me to do some tough debugging on different folks’s units with out having to construct instantly to those units from Xcode. Very quick, very helpful.
Accessing logs in your app
If you already know that you just’d like to have the ability to obtain logs from customers to be able to debug points with full entry to your log messages, you’ll be able to implement a log viewer in your app. To retrieve logs from the OSLog retailer, you need to use the OSLogStore
class to fetch your log messages.
For instance, right here’s what a easy view appears like that fetches all log messages that belong to subsystems that I’ve created for my app:
import Basis
import OSLog
import SwiftUI
struct LogsViewer: View {
let logs: [OSLogEntryLog]
init() {
let logStore = strive! OSLogStore(scope: .currentProcessIdentifier)
self.logs = strive! logStore.getEntries().compactMap { entry in
guard let logEntry = entry as? OSLogEntryLog,
logEntry.subsystem.begins(with: "com.donnywals") == true else {
return nil
}
return logEntry
}
}
var physique: some View {
Checklist(logs, id: .self) { log in
VStack(alignment: .main) {
Textual content(log.composedMessage)
HStack {
Textual content(log.subsystem)
Textual content(log.date, format: .dateTime)
}.daring()
}
}
}
}
It’s a fairly easy view nevertheless it does assist me to acquire saved log messages relatively simply. Including a view like this to your app and increasing it with an choice to export a JSON file that comprises all of your logs (based mostly by yourself Codable
fashions) could make acquiring logs out of your customers a breeze.
Logging and privateness
Typically, you may wish to log data that may very well be thought-about privateness delicate with a view to make debugging simpler. This data may not be required so that you can truly debug and profile your app. It’s a good suggestion to redact non-required private data that you just’re amassing when it’s being logged on consumer’s units.
By default, once you insert variables into your strings these variables might be thought-about as knowledge that must be redacted. Right here’s an instance:
appLogger.log(stage: .default, "Good day, world! (accessToken)")
I’m logging an entry token on this log message. Once I profile my app with the debugger hooked up, every little thing I log might be printed as you’ll anticipate; I can see the entry token.
Nonetheless, once you disconnect the debugger, launch your app, after which view your logs within the Console app when you’re not operating your app by Xcode, the log messages will look extra like this:
Good day, world! <personal>
The variable that you just’ve added to your log is redacted to guard your consumer’s privateness. If you happen to take into account the knowledge you’re inserting to be non-privacy delicate data, you’ll be able to mark the variable as public as follows:
appLogger.log(stage: .default, "Background standing: (newStatus, privateness: .public)")
On this case I need to have the ability to see the standing of my background motion handler so I have to mark this data as public.
Be aware that whether or not or not your log messages are recorded when the debugger isn’t hooked up is determined by the log stage you’re utilizing. The default
log stage will get continued and is accessible in Console app once you’re not debugging. Nonetheless, the debug
and data
log ranges are solely proven when the debugger is hooked up.
Different log ranges which are helpful once you wish to ensure you can see them even when the debugger isn’t hooked up are error
and fault
.
If you’d like to have the ability to monitor whether or not privateness delicate data stays the identical all through your app, you’ll be able to ask the logger to create a hash for the privateness delicate worth. This lets you guarantee knowledge consistency with out truly understanding the content material of what’s being logged.
You are able to do this as follows:
appLogger.log(stage: .default, "Good day, world! (accessToken, privateness: .personal(masks: .hash))")
This lets you debug knowledge consistency points with out sacrificing your consumer’s privateness which is very nice.
In Abstract
Having the ability to debug and profile your apps is important to your app’s success. Logging is a useful software that you need to use whereas growing your app to interchange your normal print
calls and it scales superbly to manufacturing conditions the place you want to have the ability to acquire collected logs out of your consumer’s units.
I extremely suggest that you just begin experimenting with Logging
at present by changing your print statements with debug
stage logging so that you just’ll be capable to apply higher filtering and looking out in addition to stream logs in your macOS console.
Don’t neglect that you would be able to make a number of Logger
objects for various elements of your app. Having the ability to filter by subsystem and class is extraordinarily helpful and makes debugging and tracing your logs a lot simpler.