Apple does not ship an OpenTelemetry exporter for Foundation Models. You can still use the same span idea: a named interval, a parent, and a few attributes. On Apple platforms, OSSignposter is the recorder that Instruments already understands.
import os
enum AITrace {
static let signposter = OSSignposter(
subsystem: "dev.edgefde.app",
category: "ai-pipeline"
)
static func span(_ name: StaticString, _ work: () async throws -> T) async rethrows -> T {
let id = signposter.makeSignpostID()
let state = signposter.beginInterval(name, id: id)
defer { signposter.endInterval(name, state) }
return try await work()
}
}
// Sketch. Confirm OSSignposter names in the installed SDK.
let reply = try await AITrace.span("ai.request") {
let packed = try await AITrace.span("ai.context_pack") { packContext() }
let text = try await AITrace.span("ai.model") { try await session.respond(to: packed) }
return try await AITrace.span("ai.guardrail") { try applyGuardrail(text) }
}
Use dotted names the way OpenTelemetry semantic conventions do: ai.request, ai.context_pack, ai.tool, ai.model, ai.guardrail. Put the tool name, model name, profile name, and prompt version on attributes or a short signpost message. Do not put the user text there.
If you later export to an OpenTelemetry collector from a debug Mac, map those same names. Production on-device builds should keep signposts cheap and empty of content. A collector is optional. The names are not.
Nest ai.tool under ai.request for each tool. If the session calls two tools, you get two child spans. That matches the Instruments tool-call tree. Pair the ai.model span with clocks from Stream tokens and measure speed. Turn a bad tool path into a sample in Test AI behavior before you ship.
Next, read a bad trace with those names.
Key concepts
- Apple does not ship an OpenTelemetry exporter. Use the span idea with OSSignposter.
- Parent
ai.requesthas children for context pack, model, and guardrail. - Nest
ai.toolper tool call. - Attributes carry tool, model, profile, and prompt version. No user text in the span body.
Takeaways
- Keep the same span names in every feature.
- Two tool calls means two
ai.toolchild spans. - Names matter even without a collector.