| WrkstrmLog | Swift‑native, multi‑backend logging with decorators and exposure controls. Backends: OSLog (Apple), SwiftLog (portable), and Print (WASM‑friendly). |
|---|
| Workflow | Status |
|---|---|
| DocC | |
| Format | |
| Build | |
| Test / Coverage |
🚦 Quick Start 🎓 Tutorials: Logging APIs · Exposure Levels · File Logging
"The most effective debugging tool is still careful thought, followed by judiciously placed print statements." —Brian Kernighan
WrkstrmLog is a logging framework for Swift that provides consistent, configurable log output across operating system + development environment combination. Optimal backends are selected at compile time. For development ease, logs are automatic in debug. For security, logs are disabled by default in release builds unless explicitly enabled.
For a narrative overview of the project's goals 🎶, see the hosted DocC article: The Problem Space.
WrkstrmLog is a flagship library for wrkstrm. It is in every library we create. We treat it as a reference for logging APIs, observability patterns, and documentation quality. DocC articles are added with every feature. This README serves as an introductory guide.
- 🌐 Adaptive logging across Linux, Xcode, macOS terminal, and WASM
- 💼 Backends: print (WASM), OSLog (Apple), SwiftLog (portable)
- 🔧 Customizable to fit specific logging requirements
- 🚀 Simple integration with Swift Package Manager
- 🔕 Optional disabled mode to silence logs
- 🚦 Global and per-logger exposure levels via
Log.globalExposureLevelandmaxExposureLevel - 🆕 Swift 6
#fileIDsupport for concise output
WrkstrmLog supports one or more backends per logger. Provide a single backend or an ordered list; when multiple are supplied the first entry (index 0) is treated as the primary.
Examples
// Single backend
let osLog = Log(system: "App", category: "UI", backends: [OSLogBackend()])
let swiftLog = Log(system: "Srv", category: "Net", backends: [SwiftLogBackend()])
// Multi-backend fan-out; primary is index 0
let capture = /* CapturingLogBackend(...) */
let composed = Log(system: "App", category: "UI", backends: [OSLogBackend(), capture])
// Runtime selection of backend kinds is also available (see next section)Configure the active backend “kinds” at runtime via Log.Inject. When multiple kinds are supplied,
index 0 is treated as the primary.
// Set an ordered list of kinds
Log.Inject.setBackends([.os, .swift])
// Convenience: set a single kind (equivalent to setBackends([.os]))
Log.Inject.setBackend(.os)
// Append/remove kinds
Log.Inject.appendBackend(.print) // -> [.os, .swift, .print]
Log.Inject.removeBackend(.swift) // -> [.os, .print]
// Clear custom selection; revert to platform default
Log.Inject.removeAllCustomBackends() // macOS/iOS: [.os]; Linux: [.swift]; WASM: [.print]
// Inspect current resolution
let kinds = Log.Inject.currentBackends() // ordered, primary = index 0Control message body formatting via a decorator. The default Decorator.Current matches the
existing format. To print only the message body without file/function/line metadata, use Plain:
var log = Log(system: "App", category: "UI", maxExposureLevel: .info, backends: [PrintLogBackend()])
log.decorator = Log.Decorator.Plain()
log.info("hello") // Prints: "App:UI:ℹ️ hello"
// JSON decorator: includes metadata (level, system, category, file, function, line,
// timestamp, thread) in a parsable JSON body
#if canImport(Foundation)
log.decorator = Log.Decorator.JSON()
log.info("hello")
// Prints: "App:UI:ℹ️ {\"level\":\"info\",\"message\":\"hello\",\"system\":\"App\",\"category\":\"UI\",\"file\":\"YourFile\",\"function\":\"yourFunc()\",\"line\":123}"
#endifUse LogGroup to forward the same message to multiple Log instances. This is handy to keep the
user-facing log as-is while also emitting a basic/plain log to another sink.
// User-facing log (default decorator)
let userLog = Log(system: "App", category: "UI", maxExposureLevel: .info, backends: [PrintLogBackend()])
// Basic log (plain body) to another sink (e.g., SwiftLog)
let basicLog = {
var logger = Log(system: "App", category: "basic", maxExposureLevel: .info, backends: [SwiftLogBackend()])
logger.decorator = Log.Decorator.Plain()
return logger
}()
let both = LogGroup([userLog, basicLog])
both.info("Launching…")Append logs to a file as newline-delimited entries. Pair with the JSON decorator for NDJSON.
#if canImport(Foundation)
import Foundation
let fileURL = URL(fileURLWithPath: "/tmp/app.log")
let fileBackend = FileLogBackend(url: fileURL)
var fileLog = Log(system: "App", category: "file", maxExposureLevel: .info, backends: [fileBackend])
fileLog.decorator = Log.Decorator.JSON() // NDJSON lines
let both = LogGroup([userLog, fileLog])
both.info("Launching…")
#endifCreate a new timestamped file per session. Filename pattern:
<base>-yyyyMMdd-HHmmss-UUID.log.
#if canImport(Foundation)
import Foundation
let logsDir = URL(fileURLWithPath: NSTemporaryDirectory())
let sessionBackend = FileLogBackend(directory: logsDir, baseName: "app")
var sessionLog = Log(system: "App", category: "session", maxExposureLevel: .info, backends: [sessionBackend])
sessionLog.decorator = Log.Decorator.JSON()
sessionLog.info("Started session at \(Date())")
print("Session log at: \(sessionBackend.url.path)")
#endifAdd WrkstrmLog as a dependency in your Package.swift file:
dependencies: [
.package(url: "https://github.com/wrkstrm/WrkstrmLog.git", .from: "2.0.0")
]Include WrkstrmLog in your target dependencies:
targets: [
.target(name: "YourTarget", dependencies: ["WrkstrmLog"]),
]-
Import the library 📥
import WrkstrmLog -
Initialize a logger ⚙️
Create a logger with your system and category. By default, each logger suppresses messages below the
.criticallevel. Set amaxExposureLevelto allow additional levels:let logger = Log(system: "YourSystem", category: "YourCategory", maxExposureLevel: .info)
-
Log messages 📝
Use the provided methods such as
debug,verbose,info,notice,warning,error, andguard.verboselogs are emitted at the debug level.logger.debug("Debug message") logger.verbose("Verbose message") logger.info("Info message") logger.notice("Notice message") logger.warning("Warning message") logger.error("Error message") Log.guard("Critical error")
Each level maps to a visual emoji and purpose:
| Level | Emoji | Description |
|---|---|---|
| trace | 🔍 | Extremely fine‑grained details (function entry/exit, loops); rarely enabled in production. |
| debug | 🐞 | Diagnostic information (config, payloads); enable while investigating or verifying behavior. |
| info | ℹ️ | General events in the application lifecycle (successful calls, completed tasks). |
| notice | 📝 | Notable events that aren’t errors or warnings (sign‑in, cache refresh). |
| warning | Potential issues that may require attention (retries, deprecated API). | |
| error | ❗ | Recoverable failures (e.g., a failed save that can be retried). |
| critical | 🚨 | Serious problems that usually halt execution or risk data loss. |
Note: the verbose helper maps to the debug level and is emitted at the same severity.
-
Disable or enable logging in production 🔇
Loggers default to
.disabledin release builds. Use the.prodoption to keep them active or the.disabledstyle for a silent logger.let active = Log(system: "YourSystem", category: "YourCategory", options: [.prod])
-
Control log level 🎚️
Set a minimum log level when creating a logger. Messages below this level are ignored. In
DEBUGbuilds, you can temporarily override a logger's level:var logger = Log(system: "YourSystem", category: "Networking", level: .error) logger.info("Ignored") Log.overrideLevel(for: logger, to: .debug) logger.info("Logged")
-
Limit log exposure 🚦
Logging is suppressed to
.criticalmessages by default. Set the global exposure level during application startup to expose additional logs. The global level is clamped by each logger'smaxExposureLevel, requiring libraries to opt in before emitting more verbose messages:Log.globalExposureLevel = .warning // Use the logging APIs to check/act on exposure if logger.isEnabled(for: .debug) { logger.debug("Debug logs may be exposed") } else { logger.info("Debug logs are currently suppressed") } // Or execute work only when enabled at a level logger.ifEnabled(for: .notice) { log in log.notice("Performing notice‑level operation…") }
The global level is configured via
Log.globalExposureLevel. Each logger exposes its opt‑in ceiling throughmaxExposureLevel, ensuring verbose logs are only emitted when both the global and per‑logger levels allow. ConfigureLog.globalExposureLevelexplicitly during startup.
-
Backend selection is compile-time; on WASM (
#if os(WASI) || arch(wasm32)) WrkstrmLog uses a print-based backend with no Foundation/OSLog/Dispatch dependencies. -
The logging API surface (trace, debug, info, notice, warning, error, critical/guard) is identical across platforms.
-
Build example (requires a Swift toolchain with WASI support):
swift build --target WrkstrmLog --triple wasm32-unknown-wasi -c release
-
Notes:
- On macOS, Xcode/Swift may write caches to
~/Libraryduring resolution/build. If running in a sandbox that blocks this, run the build outside the sandbox or allow SwiftPM caches. - No Foundation or OSLog is linked on WASM; output is emitted via
printin a stable one-line format suitable for console capture.
- On macOS, Xcode/Swift may write caches to
WrkstrmLog can be extended or modified to suit project-specific needs. Use the sample formatters as a foundation for custom implementations.
See CONTRIBUTING.md for guidelines and the PR checklist.
Developed by rismay
- Community chat: join the Wrkstrm Discord — https://discord.gg/4KhTUbt3