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:

swift
dependencies: [
    .package(url: "https://github.com/open-telemetry/opentelemetry-swift", from: "1.0.0"),
]

Quickstart

Step 1. Let's instrument the following function:

swift
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:

swift
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:

swift
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:

swift
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:

swift
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:

swift
// 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:

swift
// 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

swift
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:

swift
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:

swift
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:

swift
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:

swift
// 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:

swift
// 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:

swift
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:

swift
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
}

What's next?