Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions Sources/Instrumentation/MetricKit/MetricKitConfiguration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

#if canImport(MetricKit) && !os(tvOS) && !os(macOS)
import Foundation
import MetricKit
import OpenTelemetryApi

@available(iOS 13.0, macOS 12.0, macCatalyst 13.1, visionOS 1.0, *)
public struct MetricKitConfiguration {
public init(
useAppleStacktraceFormat: Bool = false,
tracer: Tracer? = nil
) {
self.useAppleStacktraceFormat = useAppleStacktraceFormat
self.tracer = tracer ??
OpenTelemetry.instance.tracerProvider.get(
instrumentationName: "MetricKit",
instrumentationVersion: "0.0.1"
)
}

/// The tracer to use for creating spans from MetricKit payloads.
public var tracer: Tracer

/// When true, stacktraces from crash and hang diagnostics will be reported in Apple's
/// native MetricKit JSON format instead of being transformed to the simplified OpenTelemetry format.
///
/// Default: false (stacktraces are transformed to OTel format)
public var useAppleStacktraceFormat: Bool
}
#endif
49 changes: 31 additions & 18 deletions Sources/Instrumentation/MetricKit/MetricKitInstrumentation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,34 @@

@available(iOS 13.0, macOS 12.0, macCatalyst 13.1, visionOS 1.0, *)
public class MetricKitInstrumentation: NSObject, MXMetricManagerSubscriber {
public let configuration: MetricKitConfiguration

public override init() {
self.configuration = MetricKitConfiguration()
super.init()
}

public init(configuration: MetricKitConfiguration) {
self.configuration = configuration
super.init()
}

public func didReceive(_ payloads: [MXMetricPayload]) {
for payload in payloads {
reportMetrics(payload: payload)
reportMetrics(payload: payload, configuration: configuration)
}
}

@available(iOS 14.0, macOS 12.0, macCatalyst 14.0, watchOS 7.0, *)
public func didReceive(_ payloads: [MXDiagnosticPayload]) {
for payload in payloads {
reportDiagnostics(payload: payload)
reportDiagnostics(payload: payload, configuration: configuration)
}
}
}

// MARK: - MetricKit helpers

func getMetricKitTracer() -> Tracer {
return OpenTelemetry.instance.tracerProvider.get(
instrumentationName: metricKitInstrumentationName,
instrumentationVersion: metricKitInstrumentationVersion,
)
}

/// Estimates the average value of the whole histogram.
@available(iOS 13.0, macOS 12.0, macCatalyst 13.1, visionOS 1.0, *)
func estimateHistogramAverage<UnitType>(_ histogram: MXHistogram<UnitType>) -> Measurement<
Expand All @@ -54,8 +59,8 @@
}

@available(iOS 13.0, macOS 12.0, macCatalyst 13.1, visionOS 1.0, *)
public func reportMetrics(payload: MXMetricPayload) {
let span = getMetricKitTracer().spanBuilder(spanName: "MXMetricPayload")
public func reportMetrics(payload: MXMetricPayload, configuration: MetricKitConfiguration) {
let span = configuration.tracer.spanBuilder(spanName: "MXMetricPayload")
.setStartTime(time: payload.timeStampBegin)
.startSpan()
defer { span.end(time: payload.timeStampEnd) }
Expand Down Expand Up @@ -305,7 +310,7 @@
// Signpost metrics are a little different from the other metrics, since they can have arbitrary names.
if let signpostMetrics = payload.signpostMetrics {
for signpostMetric in signpostMetrics {
let span = getMetricKitTracer().spanBuilder(spanName: "MXSignpostMetric")
let span = configuration.tracer.spanBuilder(spanName: "MXSignpostMetric")
.startSpan()
span.setAttribute(key: "signpost.name", value: signpostMetric.signpostName)
span.setAttribute(
Expand Down Expand Up @@ -347,8 +352,8 @@
}

@available(iOS 14.0, macOS 12.0, macCatalyst 14.0, visionOS 1.0, *)
public func reportDiagnostics(payload: MXDiagnosticPayload) {
let span = getMetricKitTracer().spanBuilder(spanName: "MXDiagnosticPayload")
public func reportDiagnostics(payload: MXDiagnosticPayload, configuration: MetricKitConfiguration) {
let span = configuration.tracer.spanBuilder(spanName: "MXDiagnosticPayload")
.setStartTime(time: payload.timeStampBegin)
.startSpan()
defer { span.end() }
Expand Down Expand Up @@ -407,8 +412,12 @@
let callStackTree = $0.callStackTree
let appleJson = callStackTree.jsonRepresentation()

// Transform to simplified format, fall back to original if transformation fails
let stacktraceData = transformStackTrace(appleJson) ?? appleJson
let stacktraceData: Data
if configuration.useAppleStacktraceFormat {
stacktraceData = appleJson
} else {
stacktraceData = transformStackTrace(appleJson) ?? appleJson
}
let stacktraceJson = String(decoding: stacktraceData, as: UTF8.self)

let namespacedAttrs: [String: AttributeValueConvertable] = [
Expand Down Expand Up @@ -472,8 +481,12 @@
let callStackTree = $0.callStackTree
let appleJson = callStackTree.jsonRepresentation()

// Transform to simplified format, fall back to original if transformation fails
let stacktraceData = transformStackTrace(appleJson) ?? appleJson
let stacktraceData: Data
if configuration.useAppleStacktraceFormat {
stacktraceData = appleJson
} else {
stacktraceData = transformStackTrace(appleJson) ?? appleJson
}
let stacktraceJson = String(decoding: stacktraceData, as: UTF8.self)

// Standard OTel exception attribute (without namespace prefix)
Expand Down
23 changes: 23 additions & 0 deletions Sources/Instrumentation/MetricKit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,29 @@ if #available(iOS 13.0, *) {

The instrumentation will automatically receive MetricKit payloads and convert them to OpenTelemetry spans and logs.

### Configuration

You can optionally provide a `MetricKitConfiguration` to customize the instrumentation:

```swift
if #available(iOS 13.0, *) {
let config = MetricKitConfiguration(
useAppleStacktraceFormat: false,
tracer: customTracer
)
let metricKit = MetricKitInstrumentation(configuration: config)
MXMetricManager.shared.add(metricKit)

// Store instrumentation somewhere to keep it alive, e.g.:
// AppDelegate.metricKitInstrumentation = metricKit
}
```

| Option | Default | Description |
|-----------------------------|--------------------------------|------------------------------------------------------|
| `tracer` | (a default tracer) | Custom tracer for creating spans |
| `useAppleStacktraceFormat` | `false` | Use Apple's native JSON format for stacktraces instead of OTel |

## Data Structure Overview

MetricKit reports data in two categories: **Metrics** and **Diagnostics**.
Expand Down
Loading
Loading