OpenTelemetry Logs for Swift

This document covers logging integration with OpenTelemetry Swift. The OpenTelemetry Swift Logs API is currently under development. This guide shows how to correlate your existing logs with traces.

Prerequisites

Before you start, make sure you have configured OpenTelemetry Swift to export data to Uptrace.

If you are not familiar with logs terminology like structured logging or log-trace correlation, read the introduction to OpenTelemetry Logs first.

Overview

While the native OpenTelemetry Swift Logs API is being developed, you can still achieve log-trace correlation by:

  1. Using span events - Add structured events to spans for log-like behavior
  2. Manual trace context injection - Add trace IDs to your existing logs
  3. OSLog integration - Use Apple's unified logging with trace context

Using span events as logs

Span events provide a log-like experience within the context of a trace:

swift
import OpenTelemetryApi

let tracer = OpenTelemetry.instance.tracerProvider.get(
    instrumentationName: "MyApp",
    instrumentationVersion: "1.0.0"
)

func processRequest() {
    let span = tracer.spanBuilder(spanName: "process-request").startSpan()
    defer { span.end() }

    // Info-level log
    span.addEvent(name: "log", attributes: [
        "log.severity": AttributeValue.string("INFO"),
        "log.message": AttributeValue.string("Starting request processing")
    ])

    // Debug-level log with context
    span.addEvent(name: "log", attributes: [
        "log.severity": AttributeValue.string("DEBUG"),
        "log.message": AttributeValue.string("Fetching user data"),
        "user.id": AttributeValue.string("12345")
    ])

    // Warning log
    span.addEvent(name: "log", attributes: [
        "log.severity": AttributeValue.string("WARN"),
        "log.message": AttributeValue.string("Cache miss, fetching from database")
    ])
}

Manual trace context injection

Add trace context to your existing logging infrastructure:

swift
import Foundation
import OpenTelemetryApi
import os.log

class TracedLogger {
    private let logger: Logger
    private let subsystem: String
    private let category: String

    init(subsystem: String, category: String) {
        self.subsystem = subsystem
        self.category = category
        self.logger = Logger(subsystem: subsystem, category: category)
    }

    func info(_ message: String, span: Span? = nil) {
        let traceContext = getTraceContext(span: span)
        logger.info("\(message) \(traceContext)")
    }

    func error(_ message: String, error: Error? = nil, span: Span? = nil) {
        let traceContext = getTraceContext(span: span)
        if let error = error {
            logger.error("\(message): \(error.localizedDescription) \(traceContext)")
        } else {
            logger.error("\(message) \(traceContext)")
        }
    }

    func debug(_ message: String, span: Span? = nil) {
        let traceContext = getTraceContext(span: span)
        logger.debug("\(message) \(traceContext)")
    }

    private func getTraceContext(span: Span?) -> String {
        let activeSpan = span ?? OpenTelemetry.instance.contextProvider.activeSpan
        guard let span = activeSpan, span.context.isValid else {
            return ""
        }

        return "[trace_id=\(span.context.traceId.hexString) span_id=\(span.context.spanId.hexString)]"
    }
}

// Usage
let logger = TracedLogger(subsystem: "com.myapp", category: "network")

func fetchData() {
    let span = tracer.spanBuilder(spanName: "fetch-data").startSpan()
    defer { span.end() }

    logger.info("Starting data fetch", span: span)
    // Logs: "Starting data fetch [trace_id=abc123... span_id=def456...]"

    do {
        // ... fetch logic
        logger.info("Data fetched successfully", span: span)
    } catch {
        logger.error("Failed to fetch data", error: error, span: span)
    }
}

OSLog integration

Integrate with Apple's OSLog while maintaining trace context:

swift
import Foundation
import os.log
import OpenTelemetryApi

extension OSLog {
    static let tracing = OSLog(subsystem: "com.myapp", category: "tracing")
}

struct TracingLogger {
    static func log(
        _ message: StaticString,
        type: OSLogType = .default,
        _ args: CVarArg...
    ) {
        // Get active trace context
        var traceInfo = ""
        if let span = OpenTelemetry.instance.contextProvider.activeSpan,
           span.context.isValid {
            traceInfo = " [trace_id=\(span.context.traceId.hexString)]"
        }

        // Log with trace context
        os_log(message, log: .tracing, type: type, args)

        // Also log trace info separately for correlation
        if !traceInfo.isEmpty {
            os_log("Trace context: %{public}@", log: .tracing, type: type, traceInfo)
        }
    }
}

Structured logging helper

Create a helper for structured log entries:

swift
import Foundation
import OpenTelemetryApi

struct LogEntry {
    let severity: LogSeverity
    let message: String
    var attributes: [String: AttributeValue] = [:]
    let timestamp: Date

    enum LogSeverity: String {
        case trace = "TRACE"
        case debug = "DEBUG"
        case info = "INFO"
        case warn = "WARN"
        case error = "ERROR"
        case fatal = "FATAL"
    }

    init(severity: LogSeverity, message: String) {
        self.severity = severity
        self.message = message
        self.timestamp = Date()
    }

    mutating func setAttribute(key: String, value: String) {
        attributes[key] = .string(value)
    }

    mutating func setAttribute(key: String, value: Int) {
        attributes[key] = .int(value)
    }

    func emitToSpan(_ span: Span) {
        var eventAttributes = attributes
        eventAttributes["log.severity"] = .string(severity.rawValue)
        eventAttributes["log.message"] = .string(message)

        span.addEvent(name: "log", attributes: eventAttributes, timestamp: timestamp)
    }
}

// Usage
func processOrder(orderId: String) {
    let span = tracer.spanBuilder(spanName: "process-order").startSpan()
    defer { span.end() }

    var logEntry = LogEntry(severity: .info, message: "Processing order")
    logEntry.setAttribute(key: "order.id", value: orderId)
    logEntry.emitToSpan(span)

    // Process order...

    var successLog = LogEntry(severity: .info, message: "Order processed successfully")
    successLog.setAttribute(key: "order.id", value: orderId)
    successLog.setAttribute(key: "processing_time_ms", value: 150)
    successLog.emitToSpan(span)
}

Recording errors as logs

Use semantic conventions for error logging:

swift
func handleError(_ error: Error, span: Span) {
    span.addEvent(name: "exception", attributes: [
        "exception.type": AttributeValue.string(String(describing: type(of: error))),
        "exception.message": AttributeValue.string(error.localizedDescription),
        "exception.escaped": AttributeValue.bool(false)
    ])

    span.status = .error(description: error.localizedDescription)
}

// Usage
do {
    try riskyOperation()
} catch {
    handleError(error, span: span)
}

Best practices

Include trace context in all logs

Always include trace context when available:

swift
func log(_ message: String, level: String = "INFO") {
    var logMessage = "[\(level)] \(message)"

    if let span = OpenTelemetry.instance.contextProvider.activeSpan,
       span.context.isValid {
        logMessage += " trace_id=\(span.context.traceId.hexString)"
        logMessage += " span_id=\(span.context.spanId.hexString)"
    }

    print(logMessage)
}

Use structured attributes

Prefer structured attributes over string interpolation:

swift
// Good: Structured attributes
span.addEvent(name: "log", attributes: [
    "log.message": .string("User logged in"),
    "user.id": .string(userId),
    "user.role": .string(role)
])

// Bad: String interpolation
span.addEvent(name: "log", attributes: [
    "log.message": .string("User \(userId) with role \(role) logged in")
])

Avoid logging sensitive data

Never log passwords, tokens, or PII:

swift
// Bad
span.addEvent(name: "log", attributes: [
    "log.message": .string("User authenticated"),
    "password": .string(password)  // Never do this!
])

// Good
span.addEvent(name: "log", attributes: [
    "log.message": .string("User authenticated"),
    "user.id": .string(userId)
])

What's next?