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:
- Using span events - Add structured events to spans for log-like behavior
- Manual trace context injection - Add trace IDs to your existing logs
- 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:
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:
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:
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:
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:
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:
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:
// 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:
// 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)
])