
Logging Setup
- 3 installs
- 591 repo stars
- Updated July 24, 2026
- rshankras/claude-code-apple-skills
Generates structured logging using os.log/Logger to replace print() statements, with privacy controls and Console.app integration for iOS/macOS apps.
About
Replaces print() statements with Apple's structured os.log/Logger system, adding privacy controls and Console.app integration. A developer uses it to set up proper debug and production logging in an Apple app.
- Migrates print() to os.log/Logger
- Privacy controls and Console.app integration
Logging Setup by the numbers
- 3 all-time installs (skills.sh)
- +1 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #458 of 596 Debugging skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/rshankras/claude-code-apple-skills --skill logging-setupAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3 |
|---|---|
| repo stars | ★ 591 |
| Last updated | July 24, 2026 |
| Repository | rshankras/claude-code-apple-skills ↗ |
What it does
Generates structured logging using os.log/Logger to replace print() statements, with privacy controls and Console.app integration for iOS/macOS apps.
Files
Logging Setup Generator
Replace print() statements with Apple's structured logging system (os.log/Logger) for better debugging, privacy controls, and Console.app integration.
When This Skill Activates
Use this skill when the user:
- Asks to "add logging" or "set up logging"
- Wants to "replace print statements"
- Mentions "os.log", "Logger", or "structured logging"
- Asks about "debug logging" or "production logging"
- Wants to audit print() usage in their codebase
Why Logger Over print()
| print() | Logger |
|---|---|
| Always executes | Debug logs compiled out in Release |
| No filtering | Filter by subsystem/category in Console.app |
| No privacy | .private, .public, .sensitive annotations |
| String interpolation always runs | Deferred evaluation (performance) |
| Not in Console.app | Full system integration |
Pre-Generation Checks
1. Project Context Detection
- [ ] Check deployment target (Logger requires iOS 14+ / macOS 11+)
- [ ] Search for existing Logger/os.log usage
- [ ] Identify source file locations (Sources/, App/, etc.)
2. Conflict Detection
Search for existing logging:
Glob: **/*Logger*.swift
Grep: "import OSLog" or "os_log"If found, ask user:
- Extend existing logging?
- Replace with new implementation?
- Create separate logger?
Modes of Operation
Mode 1: Audit
Find all print() statements and report:
Grep: print\s*\(Report format:
- File:line - print statement
- Severity: Info/Warning/Error (based on context)
- Suggested Logger level
Mode 2: Generate
Create logging infrastructure from scratch.
Mode 3: Migrate
Convert existing print() to Logger with suggestions.
Configuration Questions
Ask user via AskUserQuestion:
1. Categories needed?
- Network, Auth, UI, Data (defaults)
- Custom categories?
2. Include migration helpers?
- Extension on String for quick migration
- Temporary print-to-log bridge
Generation Process
Step 1: Create AppLogger.swift
Read template from templates/AppLogger.swift and customize:
- Set subsystem from Bundle.main.bundleIdentifier
- Add user-specified categories
- Include usage examples in comments
Step 2: Determine File Location
Check project structure:
- If
Sources/exists →Sources/Logging/AppLogger.swift - If
App/exists →App/Logging/AppLogger.swift - Otherwise →
Logging/AppLogger.swift
Step 3: Provide Migration Guidance
Show examples of converting common print patterns:
// Before
print("User logged in: \(email)")
// After
AppLogger.auth.info("User logged in: \(email, privacy: .private)")Output Format
After generation, provide:
Files Created
[Path]/Logging/AppLogger.swift
Integration Steps
1. Import in files: import OSLog (not needed if using AppLogger) 2. Replace print() calls with AppLogger.[category].[level]() 3. Add privacy annotations for sensitive data
Privacy Annotations Guide
.public- Safe to log (IDs, counts, non-sensitive).private- Redacted in release (emails, names).sensitive- Always redacted (passwords, tokens)
Console.app Usage
1. Open Console.app 2. Filter by subsystem: com.yourapp 3. Filter by category: Network, Auth, etc.
Testing Instructions
1. Add a test log: AppLogger.general.debug("Test log") 2. Run app, check Xcode console 3. Open Console.app, filter by your app's subsystem
References
- logger-patterns.md - Best practices and privacy levels
- migration-guide.md - Converting print() to Logger
- templates/AppLogger.swift - Template file
Logger Patterns and Best Practices
Log Levels
Use appropriate log levels for different situations:
| Level | Use Case | Release Behavior |
|---|---|---|
.debug | Development only, verbose | Compiled out |
.info | General information | Visible |
.notice | Important events | Visible, persisted |
.warning | Potential issues | Visible, persisted |
.error | Errors that need attention | Visible, persisted |
.fault | Critical failures | Visible, persisted, highlighted |
Privacy Annotations
.public
Data safe to log in production:
AppLogger.network.info("Request ID: \(requestId, privacy: .public)")
AppLogger.data.info("Items count: \(items.count, privacy: .public)").private (default)
Redacted in release builds, visible in debug:
AppLogger.auth.info("User: \(email, privacy: .private)")
AppLogger.network.debug("Response: \(responseBody, privacy: .private)").sensitive
Always redacted, even in debug:
AppLogger.auth.info("Token: \(token, privacy: .sensitive)").hash
Shows hash of value (useful for correlation without exposing data):
AppLogger.auth.info("User hash: \(userId, privacy: .private(mask: .hash))")Category Organization
Recommended Categories
enum AppLogger {
// Core
static let general = Logger(subsystem: subsystem, category: "General")
// Networking
static let network = Logger(subsystem: subsystem, category: "Network")
static let api = Logger(subsystem: subsystem, category: "API")
// User
static let auth = Logger(subsystem: subsystem, category: "Auth")
static let user = Logger(subsystem: subsystem, category: "User")
// Data
static let data = Logger(subsystem: subsystem, category: "Data")
static let cache = Logger(subsystem: subsystem, category: "Cache")
// UI
static let ui = Logger(subsystem: subsystem, category: "UI")
static let navigation = Logger(subsystem: subsystem, category: "Navigation")
// System
static let performance = Logger(subsystem: subsystem, category: "Performance")
static let lifecycle = Logger(subsystem: subsystem, category: "Lifecycle")
}Common Patterns
Network Request Logging
func fetch(_ endpoint: Endpoint) async throws -> Data {
AppLogger.network.debug("Request: \(endpoint.method) \(endpoint.path)")
do {
let (data, response) = try await session.data(for: endpoint.request)
let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0
AppLogger.network.info("Response: \(statusCode, privacy: .public) for \(endpoint.path)")
return data
} catch {
AppLogger.network.error("Request failed: \(error.localizedDescription)")
throw error
}
}Authentication Logging
func signIn(email: String, password: String) async throws {
AppLogger.auth.info("Sign in attempt for: \(email, privacy: .private)")
do {
let user = try await authService.signIn(email: email, password: password)
AppLogger.auth.notice("Sign in successful for user: \(user.id, privacy: .private(mask: .hash))")
} catch {
AppLogger.auth.warning("Sign in failed: \(error.localizedDescription)")
throw error
}
}Data Operations Logging
func save(_ items: [Item]) throws {
AppLogger.data.debug("Saving \(items.count, privacy: .public) items")
let start = CFAbsoluteTimeGetCurrent()
try context.save()
let duration = CFAbsoluteTimeGetCurrent() - start
AppLogger.data.info("Saved \(items.count, privacy: .public) items in \(duration, format: .fixed(precision: 3))s")
}Error Logging
func handleError(_ error: Error, context: String) {
if let appError = error as? AppError {
switch appError {
case .networkError(let underlying):
AppLogger.network.error("\(context): \(underlying.localizedDescription)")
case .authError(let reason):
AppLogger.auth.error("\(context): \(reason)")
case .dataError(let underlying):
AppLogger.data.error("\(context): \(underlying.localizedDescription)")
}
} else {
AppLogger.general.fault("Unexpected error in \(context): \(error)")
}
}Anti-Patterns to Avoid
Don't Log Sensitive Data as Public
// Bad
AppLogger.auth.info("Password: \(password, privacy: .public)")
// Good
AppLogger.auth.debug("Password provided: \(password.isEmpty ? "no" : "yes", privacy: .public)")Don't Use Wrong Log Levels
// Bad - Using debug for errors
AppLogger.network.debug("Critical error: \(error)")
// Good
AppLogger.network.error("Request failed: \(error)")Don't Skip Privacy for User Data
// Bad - No privacy annotation for email
AppLogger.auth.info("User email: \(email)")
// Good
AppLogger.auth.info("User email: \(email, privacy: .private)")Don't Log Excessively in Loops
// Bad
for item in items {
AppLogger.data.debug("Processing: \(item)")
}
// Good
AppLogger.data.debug("Processing \(items.count, privacy: .public) items")
// Log individual items only at trace level or when debugging specific issuesPerformance Considerations
Deferred String Interpolation
Logger uses deferred interpolation - the string is only constructed if the log will be emitted:
// This is efficient - string only built if debug logs enabled
AppLogger.data.debug("Complex object: \(expensiveDescription())")Signposts for Performance
For performance profiling, use OSSignposter:
import OSLog
let signposter = OSSignposter(logger: AppLogger.performance)
func loadData() async {
let signpostID = signposter.makeSignpostID()
let state = signposter.beginInterval("LoadData", id: signpostID)
// ... perform work ...
signposter.endInterval("LoadData", state)
}Console.app Tips
Filtering
- By subsystem:
subsystem:com.yourapp - By category:
category:Network - By level:
type:error - Combined:
subsystem:com.yourapp AND category:Auth AND type:error
Streaming Logs
# Terminal command to stream logs
log stream --predicate 'subsystem == "com.yourapp"' --level debugExporting Logs
# Export logs to file
log collect --device --output ~/Desktop/app-logs.logarchiveprint() to Logger Migration Guide
Quick Migration Reference
| print() Pattern | Logger Equivalent |
|---|---|
print("Message") | AppLogger.general.info("Message") |
print("Error: \(error)") | AppLogger.general.error("Error: \(error)") |
print("Debug: \(value)") | AppLogger.general.debug("Debug: \(value, privacy: .private)") |
print("User: \(email)") | AppLogger.auth.info("User: \(email, privacy: .private)") |
Migration by Context
Debug Output
// Before
print("DEBUG: \(someValue)")
print(">>> \(debugInfo)")
// After
AppLogger.general.debug("\(someValue, privacy: .private)")Error Logging
// Before
print("Error: \(error)")
print("Failed to load: \(error.localizedDescription)")
// After
AppLogger.general.error("Failed to load: \(error.localizedDescription)")Network Debugging
// Before
print("Request: \(url)")
print("Response: \(statusCode)")
print("Body: \(responseBody)")
// After
AppLogger.network.debug("Request: \(url, privacy: .public)")
AppLogger.network.info("Response: \(statusCode, privacy: .public)")
AppLogger.network.debug("Body: \(responseBody, privacy: .private)")User Data
// Before
print("User logged in: \(email)")
print("User ID: \(userId)")
// After
AppLogger.auth.info("User logged in: \(email, privacy: .private)")
AppLogger.auth.info("User ID: \(userId, privacy: .private(mask: .hash))")State Changes
// Before
print("State changed to: \(newState)")
// After
AppLogger.ui.debug("State changed to: \(String(describing: newState), privacy: .public)")Bulk Migration Strategy
Step 1: Audit Current print() Usage
Run this search to find all print statements:
Grep pattern: print\s*\(Step 2: Categorize by Purpose
Group print statements by their purpose:
- Debug/Development: →
.debuglevel - Information: →
.infolevel - Warnings: →
.warninglevel - Errors: →
.errorlevel
Step 3: Assign Categories
Map to appropriate logger categories:
- Network/API calls →
AppLogger.network - Authentication →
AppLogger.auth - Data operations →
AppLogger.data - UI/Navigation →
AppLogger.ui - General/Other →
AppLogger.general
Step 4: Add Privacy Annotations
Review each log for sensitive data:
- User identifiable info →
.private - Passwords/tokens →
.sensitive - IDs, counts, status codes →
.public
Common Migration Patterns
Function Entry/Exit
// Before
func processOrder(_ order: Order) {
print("Processing order: \(order.id)")
// ... work ...
print("Order processed")
}
// After
func processOrder(_ order: Order) {
AppLogger.data.debug("Processing order: \(order.id, privacy: .private(mask: .hash))")
// ... work ...
AppLogger.data.info("Order processed: \(order.id, privacy: .private(mask: .hash))")
}Conditional Logging
// Before
#if DEBUG
print("Debug info: \(value)")
#endif
// After (no #if needed - debug logs are compiled out in release)
AppLogger.general.debug("Debug info: \(value, privacy: .private)")Error Handling
// Before
do {
try something()
} catch {
print("Error: \(error)")
}
// After
do {
try something()
} catch {
AppLogger.general.error("Operation failed: \(error.localizedDescription)")
}Performance Logging
// Before
let start = Date()
// ... work ...
print("Took: \(Date().timeIntervalSince(start))s")
// After
let start = CFAbsoluteTimeGetCurrent()
// ... work ...
let duration = CFAbsoluteTimeGetCurrent() - start
AppLogger.performance.info("Operation took \(duration, format: .fixed(precision: 3))s")Temporary Migration Helper
If you want to migrate gradually, you can create a temporary bridge:
// TEMPORARY: Remove after full migration
func debugPrint(_ message: String, file: String = #file, line: Int = #line) {
#if DEBUG
let filename = (file as NSString).lastPathComponent
AppLogger.general.debug("[\(filename):\(line)] \(message)")
#endif
}
// Usage during migration
debugPrint("Old print statement") // Easy to find and replace laterVerification Checklist
After migration:
- [ ] No print() statements remain (or only intentional ones)
- [ ] All user data has privacy annotations
- [ ] Appropriate log levels used
- [ ] Categories match the code area
- [ ] App builds without warnings
- [ ] Logs appear in Console.app with correct subsystem
import OSLog
/// Centralized logging for the app using Apple's unified logging system.
///
/// Usage:
/// ```swift
/// AppLogger.network.info("Request started")
/// AppLogger.auth.debug("User: \(email, privacy: .private)")
/// AppLogger.data.error("Save failed: \(error)")
/// ```
///
/// Privacy Levels:
/// - `.public` - Safe to log (IDs, counts, status codes)
/// - `.private` - Redacted in release (emails, names) - DEFAULT
/// - `.sensitive` - Always redacted (passwords, tokens)
///
/// Log Levels:
/// - `.debug` - Development only (compiled out in release)
/// - `.info` - General information
/// - `.notice` - Important events (persisted)
/// - `.warning` - Potential issues (persisted)
/// - `.error` - Errors (persisted)
/// - `.fault` - Critical failures (persisted, highlighted)
///
enum AppLogger {
/// The subsystem identifier, typically the app's bundle identifier.
static let subsystem = Bundle.main.bundleIdentifier ?? "com.app"
// MARK: - Core Categories
/// General logging for miscellaneous events.
static let general = Logger(subsystem: subsystem, category: "General")
// MARK: - Networking
/// Network requests, responses, and connectivity.
static let network = Logger(subsystem: subsystem, category: "Network")
// MARK: - Authentication & User
/// Authentication, login, logout, session management.
static let auth = Logger(subsystem: subsystem, category: "Auth")
// MARK: - Data & Persistence
/// Data operations, persistence, caching.
static let data = Logger(subsystem: subsystem, category: "Data")
// MARK: - User Interface
/// UI events, navigation, view lifecycle.
static let ui = Logger(subsystem: subsystem, category: "UI")
// MARK: - Performance
/// Performance measurements and profiling.
static let performance = Logger(subsystem: subsystem, category: "Performance")
}
// MARK: - Usage Examples
/*
// Basic logging
AppLogger.general.info("App launched")
// With privacy for user data
AppLogger.auth.info("User signed in: \(email, privacy: .private)")
// Error logging
AppLogger.network.error("Request failed: \(error.localizedDescription)")
// Debug (compiled out in release)
AppLogger.data.debug("Loaded \(items.count, privacy: .public) items")
// Sensitive data (always redacted)
AppLogger.auth.debug("Token: \(token, privacy: .sensitive)")
// Hash for correlation without exposing data
AppLogger.auth.info("User ID: \(userId, privacy: .private(mask: .hash))")
// Formatted numbers
AppLogger.performance.info("Duration: \(seconds, format: .fixed(precision: 2))s")
*/
// MARK: - Console.app Filtering
/*
To view logs in Console.app:
1. Open Console.app
2. Select your device or simulator
3. In the search field, filter by:
- subsystem:com.yourapp
- category:Network
- type:error
Terminal streaming:
log stream --predicate 'subsystem == "com.yourapp"' --level debug
*/