A lightweight, thread-safe, privacy-conscious Swift logging framework for iOS, macOS, tvOS, and watchOS.
Features OSLog mirroring, Combine streaming, automatic sensitive field redaction, multi-format exporting, and an optional SwiftUI debug viewer.
- Features
- Architecture & Design
- Requirements
- Installation
- Usage Guide
- How It Works
- AI Agent Support
- Contributing
- Author
- License
- ⚡ Zero Third-Party Dependencies: Built exclusively on native Apple frameworks (
Foundation,Combine,OSLog,SwiftUI). - 🛡️ Thread-Safe & Non-Blocking: Dual dispatch queue architecture guarantees safety and eliminates re-entrancy deadlocks.
- 🔒 Two-Pronged Privacy Protection:
- Automatic Field Redaction: Automatically redacts values under sensitive key patterns (e.g.
password,token,secret,apiKey). - Interpolation Redaction: Call-site privacy control with
LBLogMessagestring interpolation (\(val, privacy: .private)).
- Automatic Field Redaction: Automatically redacts values under sensitive key patterns (e.g.
- 🖥️ Apple OSLog Mirroring: Forwards entries to
os.Loggerso logs appear in macOS Console.app andlog streamCLI output under custom subsystem/category tags. - 📡 Combine Event Publisher: Real-time
logsPublisherstream emitting.recordedand.clearedevents. - 📤 Multi-Format Log Exporter: Export stored history or filtered entries to
.json,.jsonLines(NDJSON), or.plainTextas in-memoryDataor written toFile. - 📱 SwiftUI Debug Viewer (
LogBirdUI): Optional, ready-to-use SwiftUI view (LBLogsView) with search, level filter, auto-scrolling, clear logs, and OS-native export triggers (iOS Share Sheet, macOS Save Panel). - 🧩 Modular Packaging: Separate
LogBird(core logic) andLogBirdUI(SwiftUI interface) SPM products. - 🚦 Build-Aware Recording: Logging is enabled by default only under
DEBUGand can be toggled at runtime or filtered by minimum severity — no code changes needed to stay silent in release.
+-------------------------------------------------------------------------+
| Call Sites |
| LogBird.log("...") / customLogger.log(...) / log("... \(secret)") |
+------------------------------------+------------------------------------+
|
v
+-------------------------------------------------------------------------+
| LogBird |
| Static Facade / Swift 6 Sendable Instance |
+------------------------------------+------------------------------------+
|
v
+-------------------------------------------------------------------------+
| LBManager |
| - Serial State Queue (dispatchQueue: access state & history) |
| - Serial Publish Queue (publishQueue: async Combine notification) |
+-------------------+--------------------+--------------------------------+
| |
+------------+ +------------+
| |
v v
+--------------+ +---------------+
| OSLog / | | In-Memory |
| os.Logger | | History |
| (Console.app)| | ([LBLog]) |
+--------------+ +-------+-------+
|
+-----------------+-----------------+
| |
v v
+--------------+ +---------------+
| Combine | | SwiftUI |
| Publisher | | LBLogsView |
| (LBLogEvent) | | (LogBirdUI) |
+--------------+ +---------------+
| Platform | Minimum Version |
|---|---|
| Swift | 5.9+ / Swift 6 Language Mode |
| iOS | 15.0+ |
| macOS | 12.0+ |
| tvOS | 15.0+ |
| watchOS | 8.0+ |
LogBird is distributed via Swift Package Manager.
In Package.swift:
dependencies: [
.package(url: "https://github.com/javiermanzo/LogBird.git", from: "2.0.0")
]Depend on the product(s) appropriate for your target:
.target(
name: "MyCoreSDK",
dependencies: [
.product(name: "LogBird", package: "LogBird") // Core logging only (no SwiftUI)
]
),
.target(
name: "MyAppUI",
dependencies: [
.product(name: "LogBirdUI", package: "LogBird") // Includes LBLogsView debug interface
]
)In Xcode, add https://github.com/javiermanzo/LogBird.git in File > Add Package Dependencies... and select LogBird and/or LogBirdUI.
For general logging, use the static methods provided by LogBird.shared:
import LogBird
// Simple info log
LogBird.log("App completed launch configuration")
// Specifying severity levels
LogBird.log("User session expired", level: .warning)Create distinct LogBird instances to scope logs by subsystem, category, or module:
let networkLogger = LogBird(subsystem: "com.myapp.network", category: "HTTPClient")
let dbLogger = LogBird(subsystem: "com.myapp.database", category: "SQLite", maxLogs: 500)
networkLogger.log("GET /api/v1/profile returned 200 OK", level: .info)
dbLogger.log("Database migration started", level: .debug)Note: Default init() automatically infers the subsystem from Bundle.main.bundleIdentifier and the category from the caller file's module (#fileID).
Supported levels in LBLogLevel (ordered from lowest to highest severity):
| Level | Emoji | Symbol | Description |
|---|---|---|---|
.debug |
🐞 | ant |
Diagnostic messages useful during development. |
.info |
ℹ️ | info.circle |
Informational messages reporting normal execution. |
.warning |
exclamationmark.triangle |
Potential issues or non-fatal anomalies. | |
.error |
❌ | xmark.octagon |
Standard runtime errors and handled failures. |
.critical |
🚨 | flame |
Severe system failures requiring immediate action. |
All logger settings (maxLogs, isEnabled, minLogLevel, redactSensitiveFields, sensitiveKeys, identifier) can be managed atomically through LBConfig:
// Configure shared instance via LBConfig
LogBird.config = LBConfig(
maxLogs: 500,
isEnabled: true,
minLogLevel: .info,
redactSensitiveFields: true,
identifier: "SESSION-99"
)
// Mutate specific properties via config or direct property forwarders
LogBird.config.minLogLevel = .warning
LogBird.config.sensitiveKeys(.add(["ssn", "passcode"]))
// Create a logger instance with a custom LBConfig
let customLogger = LogBird(
subsystem: "com.myapp.network",
category: "HTTP",
config: LBConfig(maxLogs: 200, minLogLevel: .error)
)A runtime on/off gate for the whole logger. When false, log(...) is a no-op — nothing is forwarded to OSLog, stored, or published.
// Default: enabled under DEBUG, disabled otherwise.
// LogBird.isEnabled // == build default (DEBUG)
// Force logging on permanently (e.g. field-debug builds)
LogBird.isEnabled = true
// Drive it from your own flags or custom build macros
#if INTERNAL_BETA
LogBird.isEnabled = true
#endif
LogBird.isEnabled = FeatureFlags.verboseLoggingPer-instance works the same way — flip the property right after construction:
let logger = LogBird(subsystem: "com.myapp.network", category: "HTTP")
logger.isEnabled = trueKeep only entries at or above a severity. Lower-severity entries are dropped before any work is done. LBLogLevel is ordered .debug < .info < .warning < .error < .critical.
// Silence debug & info; keep warning, error and critical
LogBird.minLogLevel = .warning
// Reset to record everything (default)
LogBird.minLogLevel = .debug- Changes apply to the next
log(...)call. isEnabledwins overminLogLevel: when disabled, nothing is recorded regardless of the floor.clearLogs()andexport()are not gated — they always operate on the recorded history, so you can still read or reset it while logging is off.- The DEBUG default uses the host app's build configuration, since the package is compiled together with it.
LogBird supports rich, typed metadata, labeled message sections, and detailed error capturing.
// 1. Extra Labeled Message Sections
let extraMessages: [LBExtraMessage] = [
LBExtraMessage(key: "Request Headers", value: "Authorization: Bearer <redacted>\nContent-Type: application/json"),
LBExtraMessage(key: "Response Body", value: "{\"status\": \"ok\"}")
]
// 2. Typed Metadata (LBValue supports String, Int, Double, Bool, URL, Array, Dictionary)
let additionalInfo: [String: LBValue] = [
"userId": 42,
"userRole": "administrator",
"isPremium": true,
"retryCount": 3,
"endpoint": .url(URL(string: "https://api.example.com/v1/user")!)
]
// 3. Error Capturing (Automatically extracts Swift, NSError, & DecodingError/EncodingError context)
do {
try JSONDecoder().decode(User.self, from: invalidData)
} catch {
LogBird.log(
"Failed to parse user profile response",
extraMessages: extraMessages,
additionalInfo: additionalInfo,
error: error,
level: .error
)
}LogBird provides two layers of data privacy out of the box:
Key names matching sensitive patterns are automatically redacted in additionalInfo, extraMessages, and error.userInfo.
Key Normalization & Substring Matching: All keys passed via
.addor.setand metadata keys evaluated during logging are automatically normalized by converting to lowercase and stripping hyphens (-), underscores (_), and whitespace (). Substring matching is then applied against configured needles:
Original Key Normalized Form Matched Needle Result ACCESS_TOKEN/access_tokenaccesstoken"token"<redacted>Refresh-Token/REFRESH_TOKENrefreshtoken"token"<redacted>Set-Cookie/set_cookiesetcookie"cookie"<redacted>X-API-KEY/X_Api_Keyxapikey"apikey"<redacted>Private_Key/PRIVATE-KEYprivatekey"privatekey"<redacted>Auth-Header/AUTH_CODEauthheader/authcode"auth"<redacted>
Reconfigure sensitive keys at any time using LBSensitiveKeysAction:
// Configure global default sensitive keys for the entire application
LogBird.setDefaultSensitiveKeys(["password", "token", "auth", "x-api-key", "my_app_secret"])
// Add custom keys to a logger instance (preserves inherited global defaults)
LogBird.sensitiveKeys(.add(["ssn", "creditCard", "passcode"]))
// Replace sensitive keys for a logger instance entirely (bypasses global defaults)
LogBird.sensitiveKeys(.set(["customSecret"]))
// Reset logger instance back to pure global default sensitive keys
LogBird.sensitiveKeys(.reset)
// Clear all sensitive keys for a logger instance (disable key-based redaction)
LogBird.sensitiveKeys(.clear)
// Read current sensitive keys set (read-only)
let currentKeys: Set<String> = LogBird.sensitiveKeys
// Logging dictionary with sensitive keys
LogBird.log("User login attempt", additionalInfo: [
"username": "johndoe",
"authToken": "secret_abc123" // Automatically replaced with "<redacted>"
])Use explicit privacy specifiers inside string interpolations to redact inline values before they are recorded:
let userEmail = "john.doe@example.com"
let sessionToken = "xyz987654"
// String interpolation with privacy controls
LogBird.log("User \(userEmail, privacy: .public) authenticated with token \(sessionToken, privacy: .private)")
// Recorded & displayed as: "User john.doe@example.com authenticated with token <redacted>"Subscribe to logsPublisher to react to log events live in your application:
import Combine
var cancellables = Set<AnyCancellable>()
LogBird.logsPublisher
.receive(on: DispatchQueue.main)
.sink { event in
switch event {
case .recorded(let log):
print("New log [\(log.level)]: \(log.message ?? "")")
case .cleared:
print("Log history was cleared")
}
}
.store(in: &cancellables)Read the full history synchronously at any time via LogBird.logs.
Export recorded entries to JSON, JSONLines (NDJSON), or Plain Text:
// 1. Export as in-memory Data (JSON format)
let output = try LogBird.export(.all, format: .json, destination: .data)
let jsonData: Data = output.data
// 2. Export to a temporary or specific file (JSONLines format)
let fileOutput = try LogBird.export(.all, format: .jsonLines, destination: .file(nil))
if let fileURL = fileOutput.fileURL {
print("Exported logs written to: \(fileURL.path)")
}
// 3. Export filtered selection as Plain Text
let errorLogs = LogBird.logs.filter { $0.level == .error }
let textOutput = try LogBird.export(.logs(errorLogs), format: .plainText, destination: .data)Import LogBirdUI to embed the ready-made debug viewer in your application:
import SwiftUI
import LogBird
import LogBirdUI
struct DeveloperSettingsView: View {
var body: some View {
LBLogsView() // Uses LogBird.shared by default
}
}
// Or scope it to a specific LogBird instance:
struct NetworkDebugView: View {
let networkLogger: LogBird
var body: some View {
LBLogsView(logBird: networkLogger)
}
}Features included in LBLogsView:
- Real-time auto-updating list of logs.
- Full-text search across messages, metadata, errors, locations, and sources.
- Level filter picker (
All,Debug,Info,Warning,Error,Critical). - Native macOS Save Panel & iOS Share Sheet integration for exporting.
- One-click log clearing.
Empty in-memory log history whenever needed (e.g., user logout or session reset):
// Clear static instance history
LogBird.clearLogs()
// Clear custom instance history
customLogger.clearLogs()- Storage: Entries are stored in a bounded in-memory array (
[LBLog]) capped atmaxLogs(default1000). Trimming happens automatically when the limit is exceeded. SettingmaxLogs = 0turns off in-memory storage while keeping Combine streaming active. - Recording Gate: Every
log(...)call is checked againstisEnabledandminLogLevelfirst. Recording is skipped entirely (no OSLog forward, no storage, no publish) when the logger is off or the entry is below the severity floor. By defaultisEnabledis on only underDEBUG. - System Logging: Every entry is formatted into a readable block and forwarded to Apple's native
os.Logger. View output in macOS Console.app or runlog stream --subsystem com.myappin Terminal. - Thread Safety: All state reads and writes are guarded by an internal serial queue (
com.logbird.accessQueue). Combine event dispatching runs asynchronously on a separate serial queue (com.logbird.publishQueue) to avoid re-entrancy deadlocks when subscriber callbacks trigger subsequent log calls.
LogBird is designed with AI coding agents and LLM integrations in mind. It includes dedicated agent documentation and skills:
- AGENTS.md: Detailed codebase map, structural invariants, concurrency rules, and agent integration recipes.
- .agents/skills/logbird/SKILL.md: Agent skill file providing full library context and code patterns for AI tools.
- .agents/skills/logbird-migration/SKILL.md: Unified migration guide and skill for upgrading LogBird integrations across breaking releases (v1.0.0 → v2.0.0 → v2.1.0).
We welcome contributions! Please review CONTRIBUTING.md for details on development environment setup, coding standards, test execution, and pull request workflows.
LogBird was created and is maintained by Javier Manzo.
LogBird is released under the MIT License. See the LICENSE file for complete details.
