Swift Logging with os.Logger: A Practical Guide to Structured Logs
Swift’s os.Logger gives you a modern way to record diagnostic information without relying on old-style print debugging. It is designed for performance, privacy, and useful filtering in Console and system log tools, which makes it a strong choice for real apps and tests.
Quick answer: Use Logger from the OSLog framework to write structured logs with severity, privacy options, and subsystem/category grouping. It is better than print() for production debugging because logs can be filtered, redacted, and collected by the system.
Difficulty: Beginner
You'll understand this better if you know: basic Swift syntax, how strings and interpolation work, and the difference between development-time debugging and production diagnostics.
1. What Is Swift Logging with os.Logger?
os.Logger is a Swift API for writing structured diagnostic messages to the unified logging system on Apple platforms. Instead of sending plain text to the console, it records logs with metadata such as subsystem, category, log level, and privacy information.
- It belongs to the OSLog framework.
- It is intended for debugging, diagnostics, and operational insight.
- It supports severity levels like debug, info, notice, error, and fault.
- It can hide sensitive values automatically when requested.
- It works well with Console.app and system log filtering tools.
Compared with print(), Logger is more suitable for real applications because it scales better and gives you more control over what is stored and displayed.
2. Why Swift Logging Matters
Logging is one of the fastest ways to understand what your app is doing in the field, during tests, or when a bug only appears on a device. Good logs can help you diagnose crashes, track state changes, and verify that important code paths ran.
Logger matters because it solves several common problems at once:
- It keeps logs structured instead of treating everything as plain text.
- It avoids exposing private values by accident.
- It lets you separate logs by subsystem and category.
- It is designed for performance-sensitive production code.
- It gives you a standard system-wide logging path instead of ad hoc console output.
For debugging, this means you can leave useful logging in the app longer without turning your codebase into a wall of unfiltered print() statements.
3. Basic Syntax or Core Idea
To use Logger, import OSLog, create a logger instance, and call one of its logging methods.
Minimal logger setup
The most common setup defines a subsystem and category so your logs are easy to search later.
import OSLoglet logger = Logger(subsystem: "com.example.MyApp", category: "networking")After you create the logger, you can write messages with methods such as debug, info, notice, error, and fault.
Logging a message
logger.info("Request started for user: \(userID)")This records a structured log message that can be filtered by subsystem, category, and severity. String interpolation still works, but the system handles it in a logging-aware way.
4. Step-by-Step Examples
Example 1: Basic debug logging
This example shows a simple message that is useful while developing a feature.
import OSLoglet logger = Logger(subsystem: "com.example.MyApp", category: "startup")func startApp() {
logger.debug("App startup began")
}This is a clean replacement for ad hoc debug output. The log is labeled with a category so you can find startup-related messages later.
Example 2: Logging a value with interpolation
Interpolation is one of the most useful parts of Logger. You can include values directly in the message.
let username = "Taylor"logger.info("Signed in user: \(username)")This produces a structured log entry, and the system can handle the interpolated value efficiently.
Example 3: Logging errors
Error logs are useful when an operation fails and you want a clear trail for debugging.
enum NetworkError: Error {
case timeout
}let error = NetworkError.timeout
logger.error("Network request failed: \(String(describing: error))")This is better than silently swallowing failures because it documents what went wrong and when.
Example 4: Using privacy controls
You can mark sensitive values so they are not exposed in logs unless explicitly allowed.
let email = "[email protected]"logger.info("User email: \(email, privacy: .private)")This is important for personal data, tokens, and other sensitive values. The log entry stays useful without exposing everything.
5. Practical Use Cases
os.Logger is a good fit when you want logs that are useful during development and still safe enough to keep in production builds.
- Tracking app startup and launch timing.
- Recording network request lifecycles and failures.
- Logging state changes in view models or service objects.
- Capturing validation errors in form flows.
- Tracing background task progress.
- Adding test-visible diagnostics in integration tests.
It is especially helpful in code paths that are hard to reproduce, because the logs stay organized and searchable instead of mixed into unrelated console output.
6. Common Mistakes
Mistake 1: Using print() for production diagnostics
print() is fine for quick experiments, but it does not give you structured metadata, privacy handling, or consistent filtering. That makes it harder to debug real apps later.
Problem: The output is plain console text, so it is easy to miss important messages and impossible to apply log categories or privacy rules.
print("User signed in: \(email)")Fix: Use Logger so the message is categorized and sensitive data can be marked private.
import OSLoglet logger = Logger(subsystem: "com.example.MyApp", category: "auth")
logger.info("User signed in: \(email, privacy: .private)")The corrected version works because it records structured logs instead of raw console text.
Mistake 2: Forgetting to import OSLog
Logger lives in the OSLog framework, so you must import it before using the type.
Problem: Without the import, Swift cannot find Logger and the code fails to compile with an error like Cannot find 'Logger' in scope.
let logger = Logger(subsystem: "com.example.MyApp", category: "network")Fix: Import OSLog before creating the logger.
import OSLoglet logger = Logger(subsystem: "com.example.MyApp", category: "network")The fix works because the framework module makes Logger available to your file.
Mistake 3: Logging sensitive values without privacy
Logs often outlive the debugging session, so sensitive data should not be written as plain text.
Problem: This logs personal data in a way that may be visible in places you did not intend, which creates privacy and security risk.
logger.error("Token: \(token)")Fix: Mark the value private so the logging system can redact it.
logger.error("Token: \(token, privacy: .private)")The corrected version works because the system can store the log while hiding the sensitive data.
7. Best Practices
Practice 1: Use a stable subsystem and meaningful category
Choose a subsystem that identifies your app or framework, then use categories for areas like networking, persistence, or UI state.
let networkLogger = Logger(subsystem: "com.example.MyApp", category: "networking")This makes logs easy to filter later and keeps related messages grouped together.
Practice 2: Match log level to importance
Use lower-severity logs for detail and higher-severity logs for actual problems. That makes your output more readable and keeps critical events easy to find.
logger.debug("Cache miss for profile image")
logger.error("Failed to load profile image")This works well because not every message deserves the same urgency.
Practice 3: Redact values that should not be exposed
If a value might be personal, secret, or compliance-sensitive, mark it private by default.
logger.info("Session ID: \(sessionID, privacy: .private)")This reduces accidental leakage and makes your logs safer to keep around.
Practice 4: Log enough context to understand the event
A message like "Failed" is usually not enough. Include the operation, identifier, or state that helps you reproduce the issue.
logger.notice("Profile save failed for userID: \(userID, privacy: .private)")Better context means fewer back-and-forth steps when you are debugging later.
8. Limitations and Edge Cases
- Logger is meant for Apple platforms that support the unified logging system, so it is not a cross-platform replacement for every logging library.
- Logs are not guaranteed to appear exactly like print() output in every environment, especially when you expect immediate console text.
- Privacy handling can hide values, which means you may need to inspect logs with the right tools and permissions.
- Some messages may not be visible in release builds the same way they are during local development, depending on filtering and logging configuration.
- Over-logging can still create noise, even though the system is more efficient than ad hoc console printing.
One common surprise is that a log may be recorded but not immediately obvious in Xcode’s console. In practice, the message often exists in the unified logging system and is easier to inspect through Console.app or filtered debugging sessions.
9. Practical Mini Project
This mini example shows a small service that logs a simulated profile load flow. It demonstrates how Logger can help trace both success and failure paths.
import Foundation
import OSLog
struct ProfileService {
let logger = Logger(subsystem: "com.example.MyApp", category: "profile")
func loadProfile(userID: String) {
logger.info("Loading profile for userID: \(userID, privacy: .private)")
let success = true
if success {
logger.notice("Profile loaded successfully")
} else {
logger.error("Profile load failed")
}
}
}
let service = ProfileService()
service.loadProfile(userID: "user-123")This example shows a simple logging flow you could expand into real networking, persistence, or authentication code. The important part is that the log messages are grouped, readable, and safe to keep in the app.
10. Key Points
- os.Logger is Swift’s modern API for structured logging on Apple platforms.
- Subsystem and category help you organize logs by feature or module.
- Log levels communicate severity and make filtering easier.
- Privacy annotations help protect sensitive values in log output.
- Logger is usually a better production choice than print().
11. Practice Exercise
- Create a Logger with subsystem com.example.MyApp and category authentication.
- Write one info message for a successful login attempt.
- Write one error message for a failed login attempt.
- Make sure the username is logged as private.
Expected output: Two structured log entries, one for success and one for failure, with the username redacted.
Hint: Use string interpolation with privacy: .private for the username.
import OSLog
let authLogger = Logger(subsystem: "com.example.MyApp", category: "authentication")
func logLoginAttempt(username: String, succeeded: Bool) {
if succeeded {
authLogger.info("Login succeeded for \(username, privacy: .private)")
} else {
authLogger.error("Login failed for \(username, privacy: .private)")
}
}
logLoginAttempt(username: "demo-user", succeeded: true)
logLoginAttempt(username: "demo-user", succeeded: false)12. Final Summary
os.Logger gives Swift developers a structured, privacy-aware, and production-friendly way to record diagnostic messages. It is a better long-term choice than scattered print() calls because it helps you organize logs, filter them by subsystem or category, and protect sensitive values.
For everyday app development, the most important habits are simple: import OSLog, give each logger a meaningful subsystem and category, choose the right log level, and redact private data when needed. Those habits make your logs easier to search, safer to keep, and more useful when you are debugging a difficult problem.
If you want to go further, the next useful topic is how to inspect logs in Console.app and how to design subsystem and category names for larger apps.