OpenTelemetry Swift Tracing
Prerequisites
Before you start, make sure you have configured OpenTelemetry Swift to export data to Uptrace.
Installation
OpenTelemetry Swift provides the tracing API for instrumenting your Swift applications with distributed tracing.
Add the required dependencies to your Package.swift:
dependencies: [
.package(url: "https://github.com/open-telemetry/opentelemetry-swift", from: "1.0.0"),
]
Quickstart
Step 1. Let's instrument the following function:
func fetchUser(userId: String) async throws -> User {
let url = URL(string: "https://api.example.com/users/\(userId)")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(User.self, from: data)
}
Step 2. Wrap the operation with a span:
import OpenTelemetryApi
let tracer = OpenTelemetry.instance.tracerProvider.get(
instrumentationName: "MyApp",
instrumentationVersion: "1.0.0"
)
func fetchUser(userId: String) async throws -> User {
let span = tracer.spanBuilder(spanName: "fetch-user").startSpan()
defer { span.end() }
let url = URL(string: "https://api.example.com/users/\(userId)")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(User.self, from: data)
}
Step 3. Record errors and set status code:
func fetchUser(userId: String) async throws -> User {
let span = tracer.spanBuilder(spanName: "fetch-user").startSpan()
defer { span.end() }
do {
let url = URL(string: "https://api.example.com/users/\(userId)")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(User.self, from: data)
} catch {
span.addEvent(name: "exception", attributes: [
"exception.type": AttributeValue.string(String(describing: type(of: error))),
"exception.message": AttributeValue.string(error.localizedDescription)
])
span.status = .error(description: error.localizedDescription)
throw error
}
}
Step 4. Record contextual information with attributes:
func fetchUser(userId: String) async throws -> User {
let span = tracer.spanBuilder(spanName: "fetch-user")
.setSpanKind(spanKind: .client)
.startSpan()
defer { span.end() }
span.setAttribute(key: "user.id", value: userId)
span.setAttribute(key: "http.method", value: "GET")
span.setAttribute(key: "http.url", value: "https://api.example.com/users/\(userId)")
do {
let url = URL(string: "https://api.example.com/users/\(userId)")!
let (data, response) = try await URLSession.shared.data(from: url)
if let httpResponse = response as? HTTPURLResponse {
span.setAttribute(key: "http.status_code", value: httpResponse.statusCode)
}
return try JSONDecoder().decode(User.self, from: data)
} catch {
span.addEvent(name: "exception", attributes: [
"exception.type": AttributeValue.string(String(describing: type(of: error))),
"exception.message": AttributeValue.string(error.localizedDescription)
])
span.status = .error(description: error.localizedDescription)
throw error
}
}
Tracer
To create spans, you need a tracer. Acquire a tracer from the tracer provider:
import OpenTelemetryApi
let tracer = OpenTelemetry.instance.tracerProvider.get(
instrumentationName: "MyApp",
instrumentationVersion: "1.0.0"
)
You can have multiple tracers, but typically you need one per app or library. The instrumentation name helps identify which component produced the spans.
Creating spans
Use the span builder to create spans:
// Create a basic span
let span = tracer.spanBuilder(spanName: "operation-name").startSpan()
// Do some work...
// End the span when done
span.end()
Span kinds
Set the span kind to indicate the type of operation:
// Server span (incoming request)
let serverSpan = tracer.spanBuilder(spanName: "handle-request")
.setSpanKind(spanKind: .server)
.startSpan()
// Client span (outgoing request)
let clientSpan = tracer.spanBuilder(spanName: "fetch-data")
.setSpanKind(spanKind: .client)
.startSpan()
// Internal span (default)
let internalSpan = tracer.spanBuilder(spanName: "process")
.setSpanKind(spanKind: .internal)
.startSpan()
// Producer span (async message producer)
let producerSpan = tracer.spanBuilder(spanName: "send-message")
.setSpanKind(spanKind: .producer)
.startSpan()
// Consumer span (async message consumer)
let consumerSpan = tracer.spanBuilder(spanName: "receive-message")
.setSpanKind(spanKind: .consumer)
.startSpan()
Creating nested spans
Build parent-child relationships between spans to represent operation hierarchies.
Manual parent setting
func parent() {
let parentSpan = tracer.spanBuilder(spanName: "parent-operation").startSpan()
defer { parentSpan.end() }
child(parentSpan: parentSpan)
}
func child(parentSpan: Span) {
let childSpan = tracer.spanBuilder(spanName: "child-operation")
.setParent(parentSpan)
.startSpan()
defer { childSpan.end() }
// Do work...
}
Using active span context
The parent-child relationship is automatically linked when using setActive:
func parent() {
let parentSpan = tracer.spanBuilder(spanName: "parent-operation")
.setActive(true)
.startSpan()
defer { parentSpan.end() }
child()
}
func child() {
// Automatically captures active span as parent
let childSpan = tracer.spanBuilder(spanName: "child-operation")
.startSpan()
defer { childSpan.end() }
// Do work...
}
Getting the current span
Access the currently active span:
if let currentSpan = OpenTelemetry.instance.contextProvider.activeSpan {
currentSpan.setAttribute(key: "additional.info", value: "some value")
}
Adding span attributes
Annotate spans with attributes to record contextual information:
let span = tracer.spanBuilder(spanName: "http-request").startSpan()
// String attributes
span.setAttribute(key: "http.method", value: "GET")
span.setAttribute(key: "http.url", value: "https://api.example.com/users")
// Integer attributes
span.setAttribute(key: "http.status_code", value: 200)
// Boolean attributes
span.setAttribute(key: "http.retry", value: false)
// Double attributes
span.setAttribute(key: "http.duration_ms", value: 45.5)
Use semantic conventions for common operations.
Setting status code
Set the status to indicate success or failure:
// Mark span as error
span.status = .error(description: "Something went wrong")
// Mark span as ok (usually not needed, unset is default)
span.status = .ok
Adding span events
Events are time-stamped annotations on a span:
// Simple event
span.addEvent(name: "cache-hit")
// Event with attributes
span.addEvent(name: "user-action", attributes: [
"action.type": AttributeValue.string("button-click"),
"button.id": AttributeValue.string("submit-form"),
"timestamp": AttributeValue.int(Int(Date().timeIntervalSince1970))
])
Recording exceptions
Use semantic conventions for recording exceptions:
do {
try riskyOperation()
} catch {
span.addEvent(name: "exception", attributes: [
"exception.type": AttributeValue.string(String(describing: type(of: error))),
"exception.message": AttributeValue.string(error.localizedDescription),
"exception.escaped": AttributeValue.bool(true)
])
span.status = .error(description: error.localizedDescription)
}
Complete example
Here's a complete example showing tracing patterns:
import Foundation
import OpenTelemetryApi
import OpenTelemetrySdk
class UserService {
private let tracer: Tracer
init() {
self.tracer = OpenTelemetry.instance.tracerProvider.get(
instrumentationName: "UserService",
instrumentationVersion: "1.0.0"
)
}
func getUser(id: String) async throws -> User {
let span = tracer.spanBuilder(spanName: "UserService.getUser")
.setSpanKind(spanKind: .internal)
.startSpan()
defer { span.end() }
span.setAttribute(key: "user.id", value: id)
do {
// Check cache first
if let cachedUser = checkCache(id: id, parentSpan: span) {
span.addEvent(name: "cache-hit")
return cachedUser
}
span.addEvent(name: "cache-miss")
// Fetch from database
let user = try await fetchFromDatabase(id: id, parentSpan: span)
// Update cache
updateCache(user: user, parentSpan: span)
return user
} catch {
span.addEvent(name: "exception", attributes: [
"exception.type": AttributeValue.string(String(describing: type(of: error))),
"exception.message": AttributeValue.string(error.localizedDescription)
])
span.status = .error(description: error.localizedDescription)
throw error
}
}
private func checkCache(id: String, parentSpan: Span) -> User? {
let span = tracer.spanBuilder(spanName: "checkCache")
.setParent(parentSpan)
.startSpan()
defer { span.end() }
span.setAttribute(key: "cache.key", value: "user:\(id)")
// Cache lookup logic...
return nil
}
private func fetchFromDatabase(id: String, parentSpan: Span) async throws -> User {
let span = tracer.spanBuilder(spanName: "fetchFromDatabase")
.setParent(parentSpan)
.setSpanKind(spanKind: .client)
.startSpan()
defer { span.end() }
span.setAttribute(key: "db.system", value: "postgresql")
span.setAttribute(key: "db.operation", value: "SELECT")
span.setAttribute(key: "db.statement", value: "SELECT * FROM users WHERE id = ?")
// Database query...
return User(id: id, name: "John Doe")
}
private func updateCache(user: User, parentSpan: Span) {
let span = tracer.spanBuilder(spanName: "updateCache")
.setParent(parentSpan)
.startSpan()
defer { span.end() }
span.setAttribute(key: "cache.key", value: "user:\(user.id)")
span.setAttribute(key: "cache.ttl_seconds", value: 3600)
// Cache update logic...
}
}
struct User: Codable {
let id: String
let name: String
}