Skip to main content

Custom Spans

Generates: Spans

Measure the duration of any operation — function calls, image processing, complex calculations — using OpenTelemetry spans.

Wrap any synchronous or async operation. The span starts before the closure runs and ends automatically when it returns or throws.

// Synchronous
let result = Pulse.shared.trackSpan(
name: "parse_json",
params: ["format": "json", "payload.size_bytes": data.count]
) {
try JSONDecoder().decode(Response.self, from: data)
}

// Async
let users = await Pulse.shared.trackSpan(name: "fetch_users") {
try await api.getUsers()
}

Manual Start / End

For spans whose lifetime spans multiple functions or async boundaries:

let span = Pulse.shared.startSpan(
name: "image_upload",
params: [
"file.size_bytes": imageData.count,
"file.format": "jpeg",
]
)

defer { span.end() }

do {
let url = try await uploadService.upload(imageData)
span.setAttribute(key: "upload.url", value: AttributeValue.string(url.absoluteString))
} catch {
span.setAttribute(key: "error", value: AttributeValue.bool(true))
span.setAttribute(key: "error.message", value: AttributeValue.string(error.localizedDescription))
throw error
}
warning

Always call span.end() on manually started spans. Use defer { span.end() } to guarantee it runs even on thrown errors.

Adding Attributes During Execution

let span = Pulse.shared.startSpan(name: "database_query")
defer { span.end() }

let results = try database.query("SELECT * FROM products WHERE category = ?", args: [category])

// Add result metadata after the operation
span.setAttribute(key: "db.rows_returned", value: AttributeValue.int(results.count))
span.setAttribute(key: "db.query_plan", value: AttributeValue.string("index_scan"))

Nested Spans

Spans created inside trackSpan are automatically nested as child spans:

await Pulse.shared.trackSpan(name: "checkout_flow") {

await Pulse.shared.trackSpan(name: "validate_cart") {
try await cart.validate()
}

await Pulse.shared.trackSpan(name: "process_payment") {
try await payment.process()
}

await Pulse.shared.trackSpan(name: "send_confirmation") {
try await notifications.sendOrderConfirmation()
}
}

Direct OpenTelemetry Access

For advanced use cases, access the underlying tracer directly:

if let otel = Pulse.shared.getOpenTelemetry() {
let tracer = otel.tracerProvider.get(
instrumentationName: "my-library",
instrumentationVersion: "1.0.0"
)

let span = tracer
.spanBuilder(spanName: "custom_operation")
.setSpanKind(spanKind: .client)
.startSpan()

// ... do work ...

span.end()
}

Generated Telemetry

Type: Span
Span Kind: Internal (unless overridden via direct OTel access)

Attributes

AttributeDescriptionAlways Present
session.idCurrent session identifier✅ Yes
screen.nameActive UIViewController at span start⚠️ If available
(your params)All key/value pairs from params⚠️ As provided

Sample Payload

{
"name": "fetch_users",
"kind": "INTERNAL",
"duration_ms": 234,
"status": "OK",
"attributes": {
"session.id": "f40364c92b85ec0c19c35a65be42b97f",
"screen.name": "HomeViewController"
}
}