Loading

Manual instrumentation using EDOT iOS

Learn how to manually instrument your app using the OpenTelemetry-Swift APIs configured by EDOT iOS.

Manual instrumentation allows you to capture app-specific operations that automatic instrumentation can't infer, such as:

  • A user completing a checkout flow
  • A local database query
  • The number of items added to a cart
  • A log record that explains why an operation failed

After completing the setup, EDOT iOS registers global tracer, meter, and logger providers. Access them through the OpenTelemetry instance:

import OpenTelemetryApi

let tracer = OpenTelemetry.instance.tracerProvider.get(
  instrumentationName: "com.example.my-app"
)

let meter = OpenTelemetry.instance.meterProvider
  .meterBuilder(name: "com.example.my-app")
  .build()

let logger = OpenTelemetry.instance.loggerProvider
  .loggerBuilder(instrumentationScopeName: "com.example.my-app")
  .build()
		

Use a stable instrumentation scope name that identifies the library or app component creating the telemetry.

A span represents an operation with a start and end time.

The following example creates a span around an operation:

import OpenTelemetryApi

func loadProducts() {
  let tracer = OpenTelemetry.instance.tracerProvider.get(
    instrumentationName: "com.example.my-app"
  )

  let span = tracer.spanBuilder(spanName: "load-products").startSpan()
  defer {
    span.end()
  }

  // Run the operation you want to measure.
}
		

Always end spans. A defer block is useful when an operation can return from multiple code paths.

Use parent and child spans to represent work performed as part of a larger operation:

import OpenTelemetryApi

func refreshProducts() {
  let tracer = OpenTelemetry.instance.tracerProvider.get(
    instrumentationName: "com.example.my-app"
  )

  let parentSpan = tracer.spanBuilder(spanName: "refresh-products").startSpan()
  defer {
    parentSpan.end()
  }

  let childSpan = tracer.spanBuilder(spanName: "read-product-cache")
    .setParent(parentSpan)
    .startSpan()

  // Read the local cache.

  childSpan.end()
}
		

Setting the parent explicitly keeps the relationship clear when work crosses queues or asynchronous boundaries.

Attributes add queryable context to a span. Events identify a meaningful point in time during the operation:

import OpenTelemetryApi

func loadProducts() {
  let tracer = OpenTelemetry.instance.tracerProvider.get(
    instrumentationName: "com.example.my-app"
  )
  let span = tracer.spanBuilder(spanName: "load-products").startSpan()
  defer {
    span.end()
  }

  span.setAttribute(key: "app.screen", value: "products")
  span.setAttribute(key: "product.count", value: 42)

  span.addEvent(
    name: "cache.miss",
    attributes: [
      "cache.name": .string("products"),
    ]
  )
}
		

Avoid putting secrets, personal data, or values with unbounded cardinality in attributes.

Record an exception event and set the span status when an operation fails:

import OpenTelemetryApi

func refreshProducts() async throws {
  let tracer = OpenTelemetry.instance.tracerProvider.get(
    instrumentationName: "com.example.my-app"
  )
  let span = tracer.spanBuilder(spanName: "refresh-products").startSpan()
  defer {
    span.end()
  }

  do {
    try await loadProducts()
    span.status = .ok
  } catch {
    span.recordException(error)
    span.status = .error(description: error.localizedDescription)
    throw error
  }
}
		

Recording an exception does not end the span. End it after all work and error handling is complete.

A metric measures a value over time. The following example records the number of products added to a cart:

import OpenTelemetryApi

func recordProductAdded() {
  let meter = OpenTelemetry.instance.meterProvider
    .meterBuilder(name: "com.example.my-app")
    .build()

  var productsAdded = meter.counterBuilder(name: "app.cart.products_added")
    .build()

  productsAdded.add(
    value: 1,
    attributes: [
      "product.category": .string("accessories"),
    ]
  )
}
		

Choose the instrument type that matches the value you are recording. Refer to the OpenTelemetry metrics API for counters, histograms, gauges, and other instruments.

A log record describes an event that occurs in your app:

import OpenTelemetryApi

func logProductsRefreshed() {
  let logger = OpenTelemetry.instance.loggerProvider
    .loggerBuilder(instrumentationScopeName: "com.example.my-app")
    .build()

  logger.logRecordBuilder()
    .setSeverity(.info)
    .setBody(.string("Products refreshed"))
    .setAttributes([
      "product.count": .int(42),
    ])
    .emit()
}
		

EDOT iOS adds the current session.id to exported log records. If a log is related to a span, set its span context so you can correlate both signals:

import OpenTelemetryApi

func logRefreshFailure() {
  let tracer = OpenTelemetry.instance.tracerProvider.get(
    instrumentationName: "com.example.my-app"
  )
  let logger = OpenTelemetry.instance.loggerProvider
    .loggerBuilder(instrumentationScopeName: "com.example.my-app")
    .build()
  let span = tracer.spanBuilder(spanName: "refresh-products").startSpan()

  logger.logRecordBuilder()
    .setSpanContext(span.context)
    .setSeverity(.error)
    .setBody(.string("Failed to refresh products"))
    .emit()

  span.end()
}
		

Use manual instrumentation for custom or proprietary code, closed-source components without instrumentation support, and application-specific business logic.

Before adding custom instrumentation for a framework or Apple API, review Automatic instrumentation. EDOT iOS might already generate the telemetry you need.

For more details about the upstream APIs, refer to: