Swift & Developer Tools
Autolog - Best logs you’ve never had
From random print statements to execution traces: automatic logging, early-exit details, and better context for humans and AI.
Get AutoLog on GitHubYou tap a button, nothing appears, and the console gives you this:
loading...
done
nilDone with what? Which function returned nil? Did it fail, finish successfully, or skip the operation? A few scattered print statements leave you guessing. For the same early-return case, AutoLog’s compact formatter gives you the execution and the branch:
main │ │ -> 🔵 ProfileService.loadProfile(hasAccess: false, id: 42)
main │ 17µs │ -> 🟢 ProfileService.loadProfile(hasAccess: false, id: 42) → nil
╰─ ↘️ !hasAccessThe function ran with hasAccess: false, returned nil, and exited through the failed access check. You can see the answer without adding a print statement inside that guard. This excerpt was captured from the public 0.10.4 package using its compact formatter, with padding compressed for readability. Timing varies between runs. The first block is an illustrative example of incomplete manual logging.
Write the function. Let the macro instrument it.
AutoLog was created to make execution visible without maintaining the same entry, exit, result, error, and timing statements in every function. The function behind that trace is small:
import AutoLog
@AutoLog
struct ProfileService {
func loadProfile(id: Int, hasAccess: Bool) -> String? {
guard hasAccess else {
return nil
}
return "Profile \(id)"
}
}@AutoLog instruments methods in the annotated type. Use @Log on an individual function when you want a narrower starting point. The macro generates Swift code at compile time, so you can inspect the actual expansion in Xcode.
Here is the shape of the injected work. This is pseudocode, with invented helper names to make the insertion points clear. Highlighted lines show instrumentation; the guard and return remain your logic.
func loadProfile(id, hasAccess) {
// INJECTED: capture arguments, source location, and call context.
execution = beginExecution(id, hasAccess)
// INJECTED: retain the outcome and finish timing on every exit.
defer { emitCompletion(execution.result, execution.branch, duration) }
guard hasAccess else {
// INJECTED: record the failed guard before the early return.
execution.branch = earlyExit(predicate: "hasAccess", entered: false)
return captureResult(nil) // INJECTED result capture; your return.
}
let result = "Profile \(id)"
return captureResult(result) // INJECTED result capture; your return.
}Throwing functions also get error capture. The useful part is coverage: the macro can account for paths that never reach a manually placed “finished” log at the bottom of the function.
Early exits deserve better than silence
A guard returning early is often intentional. Access is denied, input is empty, a prerequisite is missing, or a cache already has the result. None of those needs to throw an error. But a successful nil or false alone does not explain which path produced it.
AutoLog records the early-exit branch as metadata on the completion event, alongside its result and duration. In the opening trace, !hasAccess identifies the failed condition. The green completion means the function returned normally; it does not mean access was granted or the user’s operation succeeded.
You can add intent when the condition alone is not enough. The branch capture is automatic; this extra explanation is your choice:
@Log
func loadProfile(id: Int, hasAccess: Bool) -> String? {
guard hasAccess else {
AutoLog.earlyExit(.expected, "access denied by policy")
return nil
}
return "Profile \(id)"
}The explicit intent enriches the branch rather than creating a second automatic early-exit record. AutoLog can also capture early returns from conditional branches and the paths taken through supported if / else if / else branches that continue normally.
Better evidence for your AI assistant
An AI coding assistant given “loading, done, nil” has the same missing evidence you do. It may suggest a network failure or a decoding problem even though the function never got past its access check.
A trace with the function name, arguments, result, and failed guard gives it a concrete path to investigate. You can ask a much sharper question:
loadProfile(id: 42, hasAccess: false) returned nil.
AutoLog recorded an early exit at !hasAccess.
Trace where hasAccess is computed and explain why it was false.
Separate what this trace proves from what still needs checking.That can help an assistant connect runtime behavior to source, locate the relevant callers, and propose a focused fix or test. Consistent execution logs can also reduce the need to ask it to insert temporary diagnostics, rerun the app, and remove those diagnostics later.
The trace establishes that the guard failed. It does not establish why the permission value was false. More context or source inspection is still needed, and generated suggestions still need verification. Better logs improve the evidence available to AI; they do not make its conclusions automatically correct.
Carry the story across layers
The same approach applies across a view model, use case, repository, and data source. Instrument those boundaries and you can follow the work through the app. The runtime can show timing and attach recent log context to thrown-error events, giving failures some of their lead-up.
The optional SwiftUI helpers add the trigger. A logged button establishes an interaction source that downstream AutoLog calls can carry:
import AutoLogSwiftUI
import SwiftUI
struct ProfileView: View {
var body: some View {
Button.log("Load profile") {
_ = ProfileService().loadProfile(id: 42, hasAccess: false)
}
}
}That source helps distinguish overlapping operations started by different interactions. Coverage still depends on what you instrument, and detached work needs deliberate context handling. This is an execution trail, not a full recording of the app.
Choose the logs you actually need
Start with meaningful boundaries. Use @Omit to skip a method, omission options to exclude captured values, and deduplication to quiet repeated events. Passwords, tokens, and document contents should not become payloads simply because a function is annotated.
Runtime output filtering and compile-time emission are different controls. An explicit policy selects the event paths the macro generates. A service that only needs thrown failures can say so:
import AutoLog
import Foundation
@AutoLog(emit: .failures, measure: false)
struct DocumentService {
func read(_ url: URL) throws -> String {
try String(contentsOf: url, encoding: .utf8)
}
}This excludes successful-call events and disables generated timing work. It also means you will not see the opening example’s successful early-return trace under that failures-only policy. Choose the policy for the question you are trying to answer; do not assume zero overhead.
Keep domain-specific messages that explain decisions. Automatic instrumentation can show where a function went; a message can explain why a retry policy changed or why a cache entry was rejected. OSLog, existing log routing, and crash reporting can keep serving their own purposes.
Try it on one operation
Add the public Swift AutoLog package, starting at 0.10.4. Add the AutoLog product and, for interaction helpers, AutoLogSwiftUI. Start the runtime once in your app’s startup path:
import AutoLog
AutoLog.start()Annotate one service and follow one call, including a guard that returns early. The opening excerpt uses a console-capture logger with the package’s compact formatter; the default runtime routes console events through OSLog. Formatting and emitted events depend on configuration.
Release 0.10.4 requires Xcode 26 / Swift 6.2 or newer and supports iOS 17+, macOS 15+, and watchOS 11+. Xcode asks you to trust the macro plugin; automated builds need their own macro trust setup.
See the release README, the macro declarations, and the macro trust and CI guide for installation and API details.