XcodeLogger 2.2.0 is the Swift-first replacement for the legacy Objective-C Xcode Logger project.
This version keeps the familiar ideas:
- readable developer-focused log output
- compatibility routing for
XLog,DLog,DVLog,DDLog, andNLog - file-level and global level filtering
It deliberately changes one important architectural point from the legacy project:
- build-configuration behavior is no longer encoded inside
XcodeLogger - your app owns that decision in its own source using compile-time conditions such as
#if DEBUG XcodeLoggeronly consumes the result as a safe on/off policy
That is the main migration path away from the old Info.plist scheme-linking model.
Version 2.2.0 expands the Swift package around a synchronous core and asynchronous sink delivery model.
- scoped child loggers through
category(_:)andscoped(...) - per-sink policy for minimum levels, regex category rules, file overrides, sampling, and rate limiting
- redaction for metadata keys and message bodies before sink rendering
- async-capable sinks with a shared serial delivery coordinator for deterministic ordering
- new
FileSinkwith size-based rotation and archive retention - shipped
TestSinkfor package consumers writing logger tests - simplified build gating with
LoggerConfiguration.whenEnabled(_:) - legacy Objective-C implementation tree removed from the repository, while
Compatibility/XcodeLogger.hremains supported
The legacy implementation combined two concerns:
- legacy log-family calls such as
DLog,DVLog,DDLog,NLog, andXLog - app-specific decisions about which builds should emit logs
In this implementation those concerns are split:
- legacy call compatibility remains inside
XcodeLogger - build/scheme/configuration policy belongs to the consuming app
This implementation replaces that with:
- Swift Package Manager distribution
- structured
LoggerandLoggerConfigurationAPIs - sink-based output routing for
OSLog, stdout, and custom debug capture - app-owned compile-time build policies
The practical outcome is better:
- the library no longer needs to know your app's
Debug,Release,Staging, or custom configuration names - disabling logs for a build becomes an app decision expressed in app code
- when disabled,
Loggerexits before writing to any sink
- iOS 17+
- macOS 14+
- tvOS 17+
- watchOS 10+
- visionOS 1+
Add the package from GitHub and depend on the XcodeLogger product.
dependencies: [
.package(url: "https://github.com/codeFi/XcodeLogger.git", from: "2.2.0")
],
targets: [
.target(
name: "YourApp",
dependencies: [
.product(name: "XcodeLogger", package: "XcodeLogger")
]
)
]import XcodeLogger
let logger = Logger(configuration: LoggerConfiguration(
subsystem: "com.example.app",
minimumLevel: .information,
theme: .defaultDark
))
logger.log(
level: .warning,
category: .networking,
message: "Remote service degraded",
metadata: ["region": "eu-central"]
)If you want no log output for some builds, define that in your app target, not in XcodeLogger.
Important distinction:
XcodeLoggerstill knows how legacy calls map into modern categories- your app decides whether those categories should emit anything in
Debug,Release,Staging, or any custom build
Built-in legacy compatibility mapping:
XLog->defaultDLog->debugDVLog->developmentDDLog->debug-developmentNLog->networking
Create an app-owned file such as AppLogBuildConfiguration.swift:
import XcodeLogger
enum AppLogBuildConfiguration: LoggerBuildConfigurationProviding {
static let isLoggingEnabled: Bool = {
#if DEBUG
true
#elseif STAGING
true
#else
false
#endif
}()
}Then apply it when constructing the logger configuration:
import XcodeLogger
let configuration = LoggerConfiguration(
subsystem: "com.example.app",
minimumLevel: .information,
sinks: [
OSLogSink(subsystem: "com.example.app"),
DebugConsoleSink()
]
).applyingBuildConfiguration(AppLogBuildConfiguration.self)
let logger = Logger(configuration: configuration)Why this is the recommended approach:
- the compile-time conditions stay in the app that owns the build settings
XcodeLoggerstays generic and reusable- disabling logs is explicit and easy to audit
- the logger performs a hard early exit when
isEnabled == false
If you do not need a provider type, you can pass the resolved value directly:
let configuration = LoggerConfiguration(
subsystem: "com.example.app",
isEnabled: false
)LoggerLoggerConfigurationLoggerBuildConfigurationProvidingLoggerLevelLoggerCategoryLoggerFormattingLoggerThemeLogEventLogSourceLoggerSinkOSLogSinkDebugConsoleSinkStdoutSinkXcodeLogger
Use this when you want the best integration with Xcode and Console.app.
let logger = Logger(configuration: LoggerConfiguration(
subsystem: "com.example.app",
sinks: [
OSLogSink(subsystem: "com.example.app")
]
))Use this for terminal and CLI output.
let logger = Logger(configuration: LoggerConfiguration(
subsystem: "com.example.cli",
theme: .dracula,
sinks: [
StdoutSink()
]
))Behavior:
- ANSI colors are enabled automatically in supported terminals
- ANSI is suppressed under Xcode to avoid escape-sequence noise
Use this when you want the fully rendered line for a custom debug UI or local capture buffer.
final class LogBuffer: @unchecked Sendable {
private let lock = NSLock()
private(set) var lines: [String] = []
func append(_ line: String) {
lock.lock()
lines.append(line)
lock.unlock()
}
}
let buffer = LogBuffer()
let logger = Logger(configuration: LoggerConfiguration(
subsystem: "com.example.debug-panel",
theme: .dracula,
sinks: [
DebugConsoleSink(supportsANSIColors: false) { line in
buffer.append(line)
}
]
)).simple.simpleNoHeader.information.important.warning.error
Built-in categories:
defaultdebugdevelopmentdebug-developmentnetworking
Custom categories:
let payments = LoggerCategory(rawValue: "payments")LoggerConfiguration controls:
- hard enable/disable behavior through
isEnabled - global minimum level
- per-category minimum levels
- enabled category filtering
- global allowed-level overrides
- file-based allowed-level overrides
- theme selection
- header formatting
- timestamp formatting
- line separators
- sink selection
let configuration = LoggerConfiguration(
subsystem: "com.example.app",
enabledCategories: [.debug, .networking],
minimumLevel: .information,
categoryLevels: [
.debug: .simple,
.networking: .warning
],
theme: .defaultDark
)var configuration = LoggerConfiguration(subsystem: "com.example.app")
configuration.allowedLevelsByFile["PAYMENTSSERVICE.SWIFT"] = [.warning, .error]let configuration = LoggerConfiguration(
subsystem: "com.example.app",
formatting: LoggerFormatting(
timestampFormat: "HH:mm:ss.SSS",
headerTokens: [
.literal("["),
.label,
.literal("] "),
.timestamp,
.literal(" "),
.file,
.literal(":"),
.line,
.literal(" "),
.function
],
lineSeparatorAfterHeader: " ",
lineSeparatorAfterMessage: "\n"
)
)LoggerConfiguration.applyingEnvironment(_:) currently supports:
XCODELOGGER_LEVELXCODELOGGER_CATEGORIESXCODELOGGER_ANSI
These are runtime overrides. Build-configuration enablement should still live in app code via LoggerBuildConfigurationProviding or isEnabled.
The Objective-C macro shim lives in Compatibility/XcodeLogger.h.
Compatibility calls route into the modern logger by category and level.
Built-in compatibility mapping:
XLog->defaultDLog->debugDVLog->developmentDDLog->debug-developmentNLog->networking
That mapping is part of the library's compatibility layer and does not need per-app customization.
The old scheme-name registration methods still exist for source compatibility, but they are intentionally inert in the Swift implementation because build behavior now belongs to the consuming app.
Example:
import XcodeLogger
XcodeLogger.shared.emitCompatibilityLog(
type: .development,
level: .information,
file: "LegacyFile.m",
function: "-[LegacyObject run]",
line: 42,
message: "Legacy compatibility output"
)- macOS app:
Examples/XcodeLoggerMacDemo.xcodeproj - iOS app:
Examples/XcodeLoggeriOSDemo.xcodeproj - terminal demo sources:
Examples/XcodeLoggerTerminalDemo
Run the SwiftPM terminal demo:
swift run XcodeLoggerTerminalDemoRun the package test suite:
swift test