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.

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:

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 OSLog
let 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 OSLog
let 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.

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 OSLog
let 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 OSLog
let 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

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

11. Practice Exercise

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.