Function-call tracing for Swift, with output that converts to AppMap format.
The library has two parts:
AppMap— a@TracedSwift macro plus a small runtime. Annotated functions emit Call/Return trace lines with timing, parameters, task grouping, and parent/child call structure.appmap-transform— a command-line tool that converts the recorded trace lines into an AppMap JSON file (spec v1.12), viewable with the AppMap extensions for VS Code and JetBrains.
@Traced code ──► trace lines ──► appmap-transform ──► *.appmap.json
(os_log or
custom handler)
- Swift 6.2+
- macOS 13+ / iOS 16+ / tvOS 16+ / watchOS 9+ (runtime library)
appmap-transformruns on macOS
Add the package to your Package.swift:
dependencies: [
.package(url: "https://github.com/getappmap/appmap-swift.git", branch: "main"),
],
targets: [
.target(name: "MyApp", dependencies: [
.product(name: "AppMap", package: "appmap-swift"),
]),
]Annotate individual functions with @Traced, or a whole type with
@TracedAll:
import AppMap
@TracedAll
final class OrderService {
// Every public/internal function with a body is traced automatically.
func placeOrder(itemId: String, quantity: Int) async throws -> String {
...
}
// Override: capture an instance property instead of (sensitive) parameters.
@Traced(captures: ["accountId"])
func charge(cardNumber: String) async throws {
...
}
// Override: capture nothing.
@Traced(captures: [])
func storeSecret(value: String) {
...
}
}Notes on capture behavior:
- By default, every named parameter is recorded via
String(describing:). captures:takes a list of expressions evaluated in the function body, so bare identifiers resolve to parameters orselfproperties.captures: []records no values — use it for functions whose parameters carry sensitive content.@TracedAllskipsprivate/fileprivatefunctions (add an explicit@Tracedto opt one back in) and functions that already have@Traced.
Async calls are grouped: a task-local trace context propagates the task id and
parent call id across await boundaries and into child tasks, so nested calls
form a tree.
Tracing defaults to on in DEBUG builds, off in release. Configure it explicitly at startup:
import AppMap
// Default output: the unified logging system, subsystem "dev.appmap.trace".
TraceCollector.shared.configure(enabled: true)
// Or route lines yourself (stdout, a file, a network sink, ...):
TraceCollector.shared.configure(enabled: true, output: { line in print(line) })Custom handler — collect the lines however you like, then:
appmap-transform --name my-recording < trace.log > my-recording.appmap.jsonUnified logging — capture from a device/simulator run, then convert. The
transform scans each line for the [TRACE ...] markers, so the log command's
line prefixes don't need to be stripped:
# Live capture from the booted simulator:
xcrun simctl spawn booted log stream --style compact \
--predicate 'subsystem == "dev.appmap.trace"' > trace.log
# Or after the fact, from the Mac itself:
log show --last 5m --style compact \
--predicate 'subsystem == "dev.appmap.trace"' > trace.log
appmap-transform --name my-recording --app MyApp -o my-recording.appmap.json < trace.logOptions: --name sets the recording name shown in AppMap viewers, --app
sets the application name, -o writes to a file instead of stdout.
Open the resulting .appmap.json with the
AppMap extension for VS Code or JetBrains.
Examples/AppMapExample is a runnable end-to-end demo:
make example
# or, by hand:
swift run appmap-example | swift run appmap-transform --name example > example.appmap.jsonmake build # swift build
make test # swift test (macro expansion + runtime behavior tests)
make example # build and run the demo pipeline → example.appmap.json
make clean # swift package clean and remove example.appmap.jsonThree line types, key=value pairs, string values double-quoted:
[TRACE Component] id="OrderService#placeOrder" class="OrderService" function="placeOrder" file="MyApp/OrderService.swift" line=12 static=false
[TRACE Call] id=1 timestamp="2026-08-25T12:00:00.000Z" task_id=8412... parent=0 component="OrderService#placeOrder" itemId="apple"
[TRACE Return] id=2 callId=1 elapsed=0.014210 exception=nil
Componentis emitted once per function, on its first call.task_idgroups calls belonging to one logical task tree; the transform maps it to the AppMapthread_id.parentis present when the call was made inside another traced call.exceptionisnilor"ErrorType: message".
- Captured parameter values containing double quotes can confuse the line parser; the affected value may be truncated at the embedded quote.
- Return values are not recorded (only timing and exceptions).
- Concurrent child tasks (
async let, task groups) share their parent'sthread_id. The transform rebuilds the call tree from the recordedparent=ids, so nesting is always correct — but overlapping sibling calls are serialized in the output, since AppMap represents each thread as a simple call stack. Wall-clock overlap between siblings is not shown. os_logtruncates long messages; prefer a custom output handler when capturing large parameter values.