iOS Device Logs: How to View iPhone Logs With Logger
Last updated
To view iOS device logs, log with Logger by subsystem and category, read the output in Output > KXBuild Console in VS Code while debugging, and use kxdevice ostrace when you’re not debugging to filter by subsystem, level and process and export the result to a file you can analyze alongside crash reports. print is fine for everyday debugging. Switch to Logger, available from iOS 14, for logs you need to read without a debugger attached, filter by module and sort by severity. Logger behavior follows Apple’s official documentation, as of October 2026.
You need an iPhone that already installs and debugs with KXBuild. For connecting and trusting the phone, see Device setup.
print, NSLog or Logger: which should you use?
All three show output while you debug. They differ in whether you can still see it without a debugger, and whether you can filter it.
| API | Where it goes | While debugging (KXBuild Console) | Without debugging (kxdevice ostrace) | Filter by module and level |
|---|---|---|---|---|
print |
Standard output | Yes | No | No |
NSLog |
System log | Yes | Yes | Message text only |
Logger / os_log |
System log, with subsystem, category and level | Yes | Yes | Yes |
print only writes to standard output and never reaches the system log. When the app is launched from the Home Screen with no debugger, that output is lost. Logger writes to Apple’s unified logging system. Every entry carries a subsystem, category and level. The system uses the level to decide whether to keep an entry only in memory or also write it to disk, and it stores entries in a compressed binary format that’s turned into text only when you view it.
As a rule, use print for temporary output you’ll delete after investigating, and Logger for logs that stay in the code and that testers can read when reproducing a problem.
How do you write logs with Logger?
Logger lives in the os module and is available from iOS 14.
Create a Logger: subsystem and category
Each Logger takes two strings that let you filter out unrelated messages when you read the log.
- subsystem. Identifies a large area of functionality in the app. Apple recommends reverse-DNS notation, and you usually just use the Bundle ID, for example
com.example.myapp. - category. Identifies a module within the subsystem, for example
network,databaseorui. You choose the names.
Define all your Loggers in one file and reference them everywhere, so the names stay consistent.
1import os
2
3extension Logger {
4 private static let subsystem = Bundle.main.bundleIdentifier ?? "com.example.myapp"
5
6 static let network = Logger(subsystem: subsystem, category: "network")
7 static let database = Logger(subsystem: subsystem, category: "database")
8 static let ui = Logger(subsystem: subsystem, category: "ui")
9}
Then call the method for the level you want.
1func loadFeed() async {
2 Logger.network.info("Requesting home feed")
3 do {
4 let items = try await api.fetchFeed()
5 Logger.network.notice("Home feed returned \(items.count) items")
6 } catch {
7 Logger.network.error("Home feed request failed: \(error.localizedDescription, privacy: .public)")
8 }
9}
The five levels
Apple’s Generating log messages from your code explains that the system first keeps all messages in memory, and the more severe the level, the more likely it is written to disk. The table goes from least to most severe, and the disk column paraphrases Apple’s documentation.
| Logger method | Level | Written to disk | Use for |
|---|---|---|---|
debug |
Debug | No | Detailed information that’s only useful during development |
info |
Info | Only when collected with the log tool | Helpful but non-essential troubleshooting information |
notice, log |
Notice (Default) | Yes, up to a storage limit | Information essential for troubleshooting, such as conditions that may lead to failure |
error |
Error | Yes, up to a storage limit | Errors the code hits while running |
fault |
Fault | Yes, up to a storage limit | Bugs in the code and states that should never happen |
A few things to keep in mind.
- Once the logs on disk exceed a preset size, the system purges old messages, so don’t count on very old logs still being there.
- The debug level is very cheap, so you can log freely with it. fault and error make the system collect extra information and cost more, so don’t use them for normal flow.
- Logger also has
trace,warningandcritical, which Apple’s documentation describes as equivalent todebug,errorandfault.
Privacy modifiers
Apple’s documentation states that integers, floating-point numbers and Booleans in interpolations are shown by default, while dynamic strings and complex objects are redacted by default, so data like account names or phone numbers doesn’t end up in log files.
1let account = "user@example.com"
2let screen = "Settings"
3
4Logger.ui.notice("Opened screen \(screen, privacy: .public)") // Shown as is
5Logger.ui.notice("Current account \(account)") // Redacted by default
6Logger.ui.notice("Current account \(account, privacy: .private(mask: .hash))") // Redacted, but equal values get the same hash
- Add
privacy: .publicto strings you know aren’t sensitive, such as screen names, error descriptions and API paths. Otherwise you can’t see their contents in the device log. - Keep the default for accounts, tokens, locations and similar data, or write
.privateexplicitly. - To tell whether several entries come from the same user without exposing the value, use
.private(mask: .hash).
For the full list of options, see OSLogPrivacy.
os_log in Objective-C
In Objective-C, create a log object with os_log_create and call the function for each level. Mark string arguments as public with %{public}@.
1#import <os/log.h>
2
3static os_log_t NetworkLog(void) {
4 static os_log_t log;
5 static dispatch_once_t once;
6 dispatch_once(&once, ^{
7 log = os_log_create("com.example.myapp", "network");
8 });
9 return log;
10}
11
12os_log_info(NetworkLog(), "Starting request %{public}@", url.path);
13os_log_error(NetworkLog(), "Request failed with status %d", statusCode);
Where do iOS device logs show up while debugging?
After you press F5 to build and run, output from print, NSLog and os_log in your app appears in the KXBuild Console channel of the VS Code Output panel. The build log is in the KXBuild channel.
- Press Ctrl+Shift+U to open the Output panel.
- Choose KXBuild Console in the dropdown at the top right of the panel.
- Type a keyword in the filter box at the top right of the panel to show only lines containing it.
Ending the debug session quits the app on the phone, so KXBuild Console stops getting new output. To read logs when the app isn’t being debugged, or to filter by level or subsystem, use kxdevice as described in the next section.
How does kxdevice ostrace filter logs when the app isn’t being debugged?
kxdevice ostrace streams the phone’s os_log output live. Both NSLog and Logger go through it, it’s cleaner than syslog, and it supports filters. The app doesn’t need to be under the debugger, so it works when you open the app from the Home Screen too.
1kxdevice ostrace --subsystem com.example.myapp
Press Ctrl-C to stop. The available filters are below. The kxdevice command reference is the authoritative list.
| Option | Effect |
|---|---|
--levels default,info,debug,error,fault |
Only these levels, comma-separated |
--pid N |
Only this process, -1 for all |
--subsystem S |
Subsystem name contains S |
--match S |
Message must contain S |
--exclude S |
Message must not contain S |
default in --levels corresponds to Logger’s notice and log. There’s no separate notice value. When several phones are connected, put the global --udid option before the command to pick one.
Filter by app
If your logs use the Bundle ID as the subsystem, --subsystem is the most direct filter. To see only errors, add the levels.
1kxdevice ostrace --subsystem com.example.myapp --levels error,fault
Logs from third-party libraries and NSLog usually don’t carry your subsystem. To include them, filter by process instead. Launch the app with kxdevice, which prints its pid. If the app is already running, look it up with apps ps.
1kxdevice apps launch com.example.myapp
2kxdevice apps ps --apps
3kxdevice ostrace --pid 1234
--subsystem is a “contains” match, so com.example matches both the app and its extensions, such as widgets.
Filter by category and keyword
ostrace has no separate category option. Every output line includes “subsystem/category”, so filter by category with findstr or grep.
1kxdevice ostrace --subsystem com.example.myapp | findstr "com.example.myapp/network"
1kxdevice ostrace --subsystem com.example.myapp | grep "com.example.myapp/network"
--match and --exclude only look at the message text. Use them to filter by a business keyword or to drop noisy heartbeat logs.
1kxdevice ostrace --subsystem com.example.myapp --match order --exclude heartbeat
syslog: the whole device log
To see how system services handle your app, for example a failed install, a denied permission or the system terminating the app, use kxdevice syslog. It prints the whole device log, which is very noisy, so always filter it.
1kxdevice syslog | findstr MyApp
Export to a file
ostrace and syslog both stream live and don’t replay past entries. So start recording first, then reproduce the problem.
In PowerShell, save the output as a UTF-8 file so non-ASCII text isn’t garbled.
1kxdevice ostrace --subsystem com.example.myapp | Out-File -Encoding utf8 .\myapp-log.txt
On Linux or WSL, use tee to keep the output on screen while writing it to a file.
1kxdevice ostrace --subsystem com.example.myapp | tee myapp-log.txt
When you’ve reproduced the problem, press Ctrl-C. The file holds the logs for that period. When you send it to a teammate or attach it to an issue, include the phone model, iOS version and steps to reproduce.
If you’d rather not remember commands, run kxdevice web and open http://localhost:8780 in your browser. You can view the system log, crash reports and the process list in the web page.
How do you analyze logs together with crash reports?
After a crash, the crash report tells you which line crashed, and the log tells you what happened before the crash. Reading them side by side is much faster than looking at the stack alone.
- Open a terminal and export ostrace to a file.
- Reproduce the crash on the phone.
- Stop recording and pull the crash reports.
1kxdevice crash list MyApp*
2kxdevice crash pull .\crashes MyApp*
- Open the .ips file, note the crash time, and find the last few entries before that time in the log file.
- Look at what happened after the last normal log entry and before the crash to narrow down the suspect code.
To make this step useful, log a notice at the entry and exit of key flows and an error in failure branches. For reading crash reports, see the crash section of Debug iOS apps on Windows with LLDB, breakpoints and crash logs.
Where are logs in Flutter and React Native projects?
Cross-platform projects have two layers of logs, and you read each in a different place.
| Log source | Where to read it |
|---|---|
| print, NSLog and Logger in Swift and Objective-C code under the ios folder | Output > KXBuild Console while debugging, kxdevice ostrace otherwise |
| Flutter’s Dart logs (print, debugPrint, log from dart:developer) | The Logging view in DevTools, as the Flutter docs describe |
| React Native’s JS logs (console.log and so on) | The Console panel of React Native DevTools, opened by pressing j in the terminal running Metro |
- Flutter. Log with Logger in native plugins and MethodChannel handlers, and filter them as you would in a pure native app. On the Dart side, the
nameparameter ofdeveloper.logworks like a category and groups entries in DevTools. See Add logging to your application in the Flutter docs. For debugging steps, see Debug a Flutter iOS app on your iPhone from Windows, with hot reload. - React Native. Native module logs show up in KXBuild Console. For JS logs and JS breakpoints, use React Native’s own debugging tools. See Build a React Native iOS app on Windows and run it on your iPhone.
Log levels and when to use them
Check this table before you write a log line. Pick the right level, and --levels filters exactly what you need later.
| Situation | Logger method | ostrace --levels value |
|---|---|---|
| Printing variables or intermediate values in loops, only during development | debug |
debug |
| Recording progress, such as starting a request or reading the cache | info |
info |
| Key milestones, such as login success, a payment callback or the first screen appearing | notice |
default |
| Recoverable errors, such as a failed request or failed parsing | error |
error |
| States that should never happen, such as inconsistent data or unexpected branches beyond assertions | fault |
fault |
| Only the problems | — | error,fault |
With levels picked from this table, your iOS device logs filter down to the entries you need in both KXBuild Console and ostrace.
FAQ
How do I read an app’s logs on iPhone when it’s not being debugged?
Use kxdevice ostrace to stream the phone’s os_log live, and pass your Bundle ID to –subsystem to keep only your app’s logs. Your code must log with Logger, os_log or NSLog, because print output doesn’t show up there.
Why don’t strings logged with Logger show up?
The system treats dynamic strings as private data and redacts them by default. Add privacy: .public in the interpolation for values you know aren’t sensitive, for example (screen, privacy: .public).
Why can’t I find Logger debug and info entries later?
According to Apple’s documentation, the debug level isn’t written to disk, and info is only written when collected with the log tool. Use notice or higher for logs you need to find afterwards, and start recording with ostrace before you reproduce the problem.
Can kxdevice ostrace filter by category?
There’s no separate category option. Every output line includes the subsystem and category, so filter your app first with –subsystem, then filter by “subsystem/category” with findstr or grep.
How do I export iPhone logs to a file to send to someone?
Start recording before you reproduce the problem. In PowerShell, run kxdevice ostrace –subsystem YourBundleID | Out-File -Encoding utf8 .\log.txt, on Linux use tee, and press Ctrl-C when you’re done.