OpenTelemetry Metrics for Swift
This document covers the OpenTelemetry Metrics API for Swift, including how to create instruments (Counter, Histogram, Gauge) and configure them with labels.
Prerequisites
Before you start, make sure you have configured OpenTelemetry Swift to export data to Uptrace.
If you are not familiar with metrics terminology like timeseries or additive/synchronous/asynchronous instruments, read the introduction to OpenTelemetry Metrics first.
Getting started
To get started with metrics, you need a MeterProvider which you can use to create meters:
import OpenTelemetryApi
let meter = OpenTelemetry.instance.meterProvider.get(
instrumentationName: "MyApp",
instrumentationVersion: "1.0.0"
)
Using the meter, you can create instruments to measure performance. A simple Counter looks like this:
let counter = meter.createIntCounter(name: "app.requests_total")
// Increment the counter
counter.add(value: 1, labels: ["method": "GET", "status": "200"])
Counter
Counter is a synchronous instrument that measures additive non-decreasing values.
// Create an integer counter
let requestCounter = meter.createIntCounter(name: "http.requests_total")
// Increment by 1
requestCounter.add(value: 1, labels: ["method": "GET", "endpoint": "/api/users"])
// Increment by more
requestCounter.add(value: 5, labels: ["method": "POST", "endpoint": "/api/orders"])
Counter with labels
Labels allow you to slice and filter metrics:
let cacheCounter = meter.createIntCounter(name: "cache.operations_total")
func cacheOperation(hit: Bool) {
let result = hit ? "hit" : "miss"
cacheCounter.add(value: 1, labels: ["result": result])
}
// Usage
cacheOperation(hit: true) // cache.operations_total{result="hit"} += 1
cacheOperation(hit: false) // cache.operations_total{result="miss"} += 1
DoubleCounter
For fractional values, use DoubleCounter:
let dataTransferred = meter.createDoubleCounter(name: "network.bytes_transferred")
dataTransferred.add(value: 1024.5, labels: ["direction": "outbound"])
UpDownCounter
UpDownCounter measures values that can increase or decrease:
let activeConnections = meter.createIntUpDownCounter(name: "connections.active")
// Connection opened
activeConnections.add(value: 1, labels: ["type": "websocket"])
// Connection closed
activeConnections.add(value: -1, labels: ["type": "websocket"])
Histogram
Histogram records a distribution of values:
let requestDuration = meter.createDoubleHistogram(name: "http.request_duration_ms")
// Record request duration
func handleRequest() {
let startTime = Date()
// Process request...
let duration = Date().timeIntervalSince(startTime) * 1000
requestDuration.record(value: duration, labels: ["method": "GET", "endpoint": "/api/users"])
}
Custom histogram boundaries
Configure histogram bucket boundaries for your use case:
let histogram = meter.createDoubleHistogram(
name: "http.response_size_bytes",
boundaries: [100, 500, 1000, 5000, 10000, 50000]
)
Observable instruments
Observable instruments report values when the metrics are collected, rather than when they change.
ObservableGauge
For values that are sampled at collection time:
// Register a gauge that reports memory usage
let _ = meter.createIntObservableGauge(name: "process.memory_bytes") { observer in
let memoryUsage = getMemoryUsage()
observer.observe(value: memoryUsage, labels: ["type": "heap"])
}
func getMemoryUsage() -> Int {
var info = mach_task_basic_info()
var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size) / 4
let result = withUnsafeMutablePointer(to: &info) {
$0.withMemoryRebound(to: integer_t.self, capacity: 1) {
task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
}
}
return result == KERN_SUCCESS ? Int(info.resident_size) : 0
}
ObservableCounter
For monotonically increasing values read at collection time:
let _ = meter.createIntObservableCounter(name: "process.cpu_time_ms") { observer in
let cpuTime = getCPUTime()
observer.observe(value: cpuTime, labels: [:])
}
ObservableUpDownCounter
For values that can increase or decrease, read at collection time:
let _ = meter.createIntObservableUpDownCounter(name: "queue.size") { observer in
observer.observe(value: messageQueue.count, labels: ["queue": "main"])
}
Complete example
Here's a complete example demonstrating various metrics patterns:
import Foundation
import OpenTelemetryApi
import OpenTelemetrySdk
class MetricsExample {
private let meter: Meter
private let requestCounter: IntCounter
private let requestDuration: DoubleHistogram
private let activeRequests: IntUpDownCounter
init() {
self.meter = OpenTelemetry.instance.meterProvider.get(
instrumentationName: "MyApp",
instrumentationVersion: "1.0.0"
)
// Create instruments
self.requestCounter = meter.createIntCounter(name: "http.requests_total")
self.requestDuration = meter.createDoubleHistogram(name: "http.request_duration_ms")
self.activeRequests = meter.createIntUpDownCounter(name: "http.requests_active")
// Register observable gauge for memory
let _ = meter.createIntObservableGauge(name: "process.memory_bytes") { observer in
observer.observe(value: self.getMemoryUsage(), labels: [:])
}
}
func handleRequest(method: String, endpoint: String) async {
let labels = ["method": method, "endpoint": endpoint]
// Track active requests
activeRequests.add(value: 1, labels: labels)
defer { activeRequests.add(value: -1, labels: labels) }
let startTime = Date()
// Simulate request processing
try? await Task.sleep(nanoseconds: UInt64.random(in: 10_000_000...100_000_000))
let duration = Date().timeIntervalSince(startTime) * 1000
// Record metrics
requestCounter.add(value: 1, labels: labels.merging(["status": "200"]) { $1 })
requestDuration.record(value: duration, labels: labels)
}
private func getMemoryUsage() -> Int {
var info = mach_task_basic_info()
var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size) / 4
let result = withUnsafeMutablePointer(to: &info) {
$0.withMemoryRebound(to: integer_t.self, capacity: 1) {
task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
}
}
return result == KERN_SUCCESS ? Int(info.resident_size) : 0
}
}
// Usage
@main
struct App {
static func main() async {
// Configure OpenTelemetry first (see index.md)
let metrics = MetricsExample()
// Simulate traffic
for _ in 0..<10 {
await metrics.handleRequest(method: "GET", endpoint: "/api/users")
await metrics.handleRequest(method: "POST", endpoint: "/api/orders")
}
}
}
Best practices
Use meaningful metric names
Follow the naming conventions:
// Good: descriptive with units
meter.createDoubleHistogram(name: "http.request_duration_ms")
meter.createIntCounter(name: "db.queries_total")
meter.createIntObservableGauge(name: "process.memory_bytes")
// Bad: unclear names
meter.createDoubleHistogram(name: "duration")
meter.createIntCounter(name: "count")
Use consistent labels
// Define label keys as constants
struct MetricLabels {
static let method = "method"
static let endpoint = "endpoint"
static let status = "status"
}
requestCounter.add(value: 1, labels: [
MetricLabels.method: "GET",
MetricLabels.endpoint: "/api/users",
MetricLabels.status: "200"
])
Limit label cardinality
Avoid labels with high cardinality (many unique values):
// Bad: user_id has unbounded cardinality
counter.add(value: 1, labels: ["user_id": userId])
// Good: bounded cardinality
counter.add(value: 1, labels: ["user_type": userType])