# Swift Logging with os.Logger: A Practical Guide to Structured Logs

Source: https://devdocs10.com/articles/swift/swift-logging-os-logger  
By Murat Bo · Published 2026-06-15 · DevDocs10 (https://devdocs10.com)

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

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.

```swift
import OSLog
```

```swift
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

```swift
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.

```swift
import OSLog
```

```swift
let logger = Logger(subsystem: "com.example.MyApp", category: "startup")
```

```swift
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.

```swift
let username = "Taylor"
```

```swift
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.

```swift
enum NetworkError: Error {
    case timeout
}
```

```swift
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.

```swift
let email = "person@example.com"
```

```swift
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.

```swift
print("User signed in: \(email)")
```

Output (tested with Swift 6.4):

```text
User signed in: person@example.com
```

**Fix:** Use `Logger` so the message is categorized and sensitive data can be marked private.

```swift
import OSLog
```

```swift
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`.

```swift
let logger = Logger(subsystem: "com.example.MyApp", category: "network")
```

**Fix:** Import `OSLog` before creating the logger.

```swift
import OSLog
```

```swift
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.

```swift
logger.error("Token: \(token)")
```

**Fix:** Mark the value private so the logging system can redact it.

```swift
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.

```swift
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.

```swift
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.

```swift
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.

```swift
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.

```swift
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.

```swift
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.
