Honeycomb wrapper for OpenTelemetry on iOS and macOS.
STATUS: this library is in BETA. Data shapes are stable and safe for production. We are actively seeking feedback to ensure usability.
If you're using Xcode to manage dependencies...
- Select "Add Package Dependencies..." from the "File" menu.
- In the search field in the upper right, labeled “Search or Enter Package URL”, enter the Swift Honeycomb OpenTelemetry package url: https://github.com/honeycombio/honeycomb-opentelemetry-swift
- Add a project dependency on
Honeycomb
.
If you're using Package.swift
to manage dependencies...
- Add the Package dependency.
dependencies: [
.package(url: "https://github.com/honeycombio/honeycomb-opentelemetry-swift.git",
from: "0.0.6")
],
- Add the target dependency.
dependencies: [
.product(name: "Honeycomb", package: "honeycomb-opentelemetry-swift"),
],
To configure the SDK in your App
class:
import Honeycomb
@main
struct ExampleApp: App {
init() {
do {
let options = try HoneycombOptions.Builder()
.setAPIKey("YOUR-API-KEY")
.setServiceName("YOUR-SERVICE-NAME")
.build()
try Honeycomb.configure(options: options)
} catch {
NSException(name: NSExceptionName("HoneycombOptionsError"), reason: "\(error)").raise()
}
}
}
To manually send a span:
let tracerProvider = OpenTelemetry.instance.tracerProvider.get(
instrumentationName: "YOUR-INSTRUMENTATION-NAME",
instrumentationVersion: nil
)
let span = tracerProvider.spanBuilder(spanName: "YOUR-SPAN-NAME").startSpan()
span.end()
Option | Type | Required? | Description |
---|---|---|---|
tracesAPIKey |
String | No | Dedicated API Key to use when sending traces. |
metricsAPIKey |
String | No | Dedicated API Key to use when sending metrics. |
logsAPIKey |
String | No | Dedicated API Key to use when sending logs. |
dataset |
String | No | Name of Honeycomb dataset to send traces to. Required if sending to a classic Honeycomb environment. |
metricsDataset |
String | No | Name of Honeycomb dataset to send metrics to, instead of dataset . |
tracesEndpoint |
String | No | API endpoint to send traces to. |
metricsEndpoint |
String | No | API endpoint to send metrics to. |
logsEndpoint |
String | No | API endpoint to send trace to. |
sampleRate |
Int | No | Sample rate to apply (ie. a value of 40 means 1 in 40 traces will be exported). |
debug |
Boolean | No | Enable debug logging. |
serviceName |
String? | No | This determines the Honeycomb service to send data to, and also appears as the contents of the service.name resource attribute. |
serviceVersion |
String? | No | Current version of your application. Appears as the value of the service.version resource attribute. |
resourceAttributes |
Map<String, String> | No | Attributes to attach to outgoing resources. |
headers |
Map<String, String> | No | Headers to include on exported data. |
tracesHeaders |
Map<String, String> | No | Headers to add to exported trace data. |
metricsHeaders |
Map<String, String> | No | Headers to add to exported metrics data. |
logsHeaders |
Map<String, String> | No | Headers to add to exported logs data. |
timeout |
Duration | No | Timeout used by exporter when sending data. |
tracesTimeout |
Duration | No | Timeout used by traces exporter. Overrides timeout for trace data. |
metricsTimeout |
Duration | No | Timeout used by metrics exporter. Overrides timeout for metrics data. |
logsTimeout |
Duration | No | Timeout used by logs exporter. Overrides timeout for logs data. |
protocol |
HoneycombOptions.OtlpProtocol | No | Protocol to use when sending data. |
tracesProtocol |
HoneycombOptions.OtlpProtocol | No | Overrides protocol for trace data. |
metricsProtocol |
HoneycombOptions.OtlpProtocol | No | Overrides protocol for metrics data. |
logsProtocol |
HoneycombOptions.OtlpProtocol | No | Overrides protocol for logs data. |
spanProcessor |
OpenTelemetryApi.SpanProcessor | No | Additional span processor to use. |
sessionTimeout |
TimeInterval | No | Maximum length of time for a single user session. Used to generate session.id span attribute. |
metricKitInstrumentationEnabled |
Bool | No | Whether to enable MetricKit instrumentation. (default: true) |
urlSessionInstrumentationEnabled |
Bool | No | Whether to enable URLSession instrumentation. (default: true) |
uiKitInstrumentationEnabled |
Bool | No | Whether to enable UIKit view instrumentation. (default: true) |
touchInstrumentationEnabled |
Bool | No | Whether to enable UIKit touch instrumentation (default: false) |
unhandledExceptionInstrumentationEnabled |
Bool | No | Whether to enable unhandle exception instrumentation. (default: true) |
offlineCachingEnabled |
Bool | No | Whether to enable offline caching for telemetry (default: false). Warning: this feature is still in alpha and may be unstable. For more details, see Offline Caching |
All spans will include the following attributes
honeycomb.distro.runtime_version
: Version of iOS on the device. See alsoos.description
.honeycomb.distro.version
: Version of the Honeycomb SDK being used.os.description
: String containing iOS version, build ID, and SDK level.os.name
: "iOS"os.type
: "darwin"os.version
: Current iOS Versionservice.name
: The name of your application, as provided viasetServiceName()
, or inferred from your bundle if unset.service.version
: Optional. The version of your application, as provided via `setServiceVersion(), or inferred from your bundle if unset.telemetry.sdk.language
: "swift"telemetry.sdk.name
: "opentelemetry"telemetry.sdk.version
: Version of the OpenTelemetry Swift SDK being used.- UIDevice attributes (only available on platforms where
UIKit
is available):device.id
: UIDevice.identifierForVendordevice.name
- UIDevice.namedevice.systemName
- UIDevice.systemNamedevice.systemVersion
- UIDevice.systemVersiondevice.model
- UIDevice.modeldevice.localizedModel
- UIDevice.localizedModeldevice.userInterfaceIdiom
- UIDevice.userInterfaceIdiomdevice.isMultitaskingSupported
- UIDevice.isMultitaskingSupporteddevice.orientation
- UIDevice.orientationdevice.isLowPowerModeEnabled
- If the user has Low Power Mode enabled on their device.device.isBatteryMonitoringEnabled
- UIDevice.isBatteryMonitoringEnableddevice.batteryLevel
- UIDevice.batteryLevel. Only included ifUIDevice.current.batteryStateAttributesEnabled
is set totrue
.device.batteryState
- UIDevice.batteryState. Only included ifUIDevice.current.batteryStateAttributesEnabled
is set totrue
.
The following auto-instrumentation libraries are automatically included:
- MetricKit data is automatically collected.
- Some UIKit controls are automatically instrumented as described below.
UIKit views will automatically be instrumented, emitting viewDidAppear
and viewDidDisappear
events. Both have the following attributes:
view.title
- Title of the view controller, if provided.view.nibName
- The name of the view controller's nib file, if one was specified.view.animated
- true if the transition to/from this view is animated, false if it isn't.view.class
- name of the swift/objective-c class this view controller has.screen.name
- name of the screen that appeared. In order of precedence, this attribute will have the value of the first of these to be set:accessiblityIdentifier
of the view that appearedview.title
- as defined above. If the view is a UINavigationController, Storybook Identifier (below) will be used in preference toview.title
.- Storybook Identifier - unique id identifying the view controller within its Storybook.
view.class
- as defined above
screen.path
- the full path leading to the current view, consisting of the current view'sscreen.name
as well as any parent views.
viewDidAppear
events will also track screen.name
as the "current screen" (as with the manual instrumentation described below), and will include that value as screen.name
on other, non-navigation spans emitted.
Various touch events are instrumented, such as:
Touch Began
- A touch startedTouch Ended
- A touch endedclick
- A "click". This is currently ony instrumented forUIButton
s.
These events may have the following attributes. In the case of name attributes, we may walk up the view hierarchy to find a valid entry.
view.class
: e.g."UIButton"
view.accessibilityIdentifier
, TheaccessibilityIdentifier
property of aUIView
, e.g."accessibleButton"
view.accessibilityLabel
- TheaccessibilityLabel
property of aUIView
, e.g."Accessible Button"
view.currentTitle
- ThecurrentTitle
property of aUIButton
.view.titleLabel.text
- Thetext
of aUIButton
'stitleLabel
, e.g."Accessible Button"
view.name
: The "best" available name of the view, given the other identifiers, e.g."accessibleButton"
The default session manager will create a new session on startup and will expire the session after a timeout.
You can call HoneycombOptions.setSessionTimeout
to set the timeout duration.
Spans will have the following attributes:
session.id
will be added to spans
You can subscribe to .sessionStarted
and sessionEnded
with NotificationCenter
to be notified of session start and end events.
For .sessionStarted
:
userInfo["session"]
contains the session just createduserInfo["previousSession"]
contains the previous session ornil
if there is no previous session. For.sessionEnded
:userInfo["previousSession"]
contains the session just ended.
Network events on URLSession
will automatically be instrumented.
If you are connecting your app to a backend service that you wish to view as a unified trace with your app, you will need to manually add headers to all your outgoing requests. You must also create a span and set it as the active span. The span's context will be used to generate the headers needed for trace propagation.
Below is an example of adding the headers to a network request.
import OpenTelemetryApi
private struct HttpTextMapSetter: Setter {
func set(carrier: inout [String: String], key: String, value: String) {
carrier[key] = value
}
}
private let textMapSetter = HttpTextMapSetter()
func makeBackendRequest(data: Data) async throws {
let url = URL(string: "https://mybackendservice")
var request = URLRequest(url: url!)
request.httpMethod = "POST"
request.httpBody = data
let allHeaders: [String: String] = []
let span = OpenTelemetry.instance.tracerProvider.get(
instrumentationName: "mybackendservice.network",
instrumentationVersion: getCurrentAppVersion()
)
.spanBuilder(spanName: "backendRequest")
// The span must be made the active span or else the network autoinstrumentation
// will not be attached to the trace.
.setActive(true)
.startSpan()
defer {
span.end()
}
// This will add the required headers to the `allHeaders` Dictionary
OpenTelemetry.instance.propagators.textMapPropagator.inject(
spanContext: span.context,
carrier: &allHeaders,
setter: textMapSetter
)
allHeaders.forEach({ (key: String, value: String) in
request.setValue(value, forHTTPHeaderField: key)
})
let session = URLSession(configuration: URLSessionConfiguration.default)
let (data, response) = try await session.data(for: request)
// process your response data as normal
}
Wrap your SwiftUI views with HoneycombInstrumentedView(name: String)
, like so:
var body: some View {
HoneycombInstrumentedView(name: "main view") {
VStack {
// ...
}
}
}
This will measure and emit instrumentation for your View's render times, ex:
Specifically, it will emit 2 kinds of span for each view that is wrapped:
View Render
spans encompass the entire rendering process, from initialization to appearing on screen. They include the following attributes:
view.name
(string): the name passed toHoneycombInstrumentedView
view.renderDuration
(double): amount of time to spent initializing the contents ofHoneycombInstrumentedView
view.totalDuration
(double): amount of time from whenHoneycombInstrumentedView.body()
is called to when the contents appear on screen
View Body
spans encompass just the body()
call of `HoneycombInstrumentedView, and include the following attributes:
view.name
(string): the name passed toHoneycombInstrumentedView
iOS 16 introduced two new Navigation types that replace the now-deprecated NavigationView.
We offer a convenience view modifier (.instrumentNavigation(path: String)
) for cases where you are using a NavigationStack
and managing navigation state externally
ex.:
import Honeycomb
struct SampleNavigationView: View {
@State private var presentedParks: [Park] = [] // or var presentedParks = NavigationPath()
var body: some View {
NavigationStack(path: $presentedParks) {
List(PARKS) { park in
NavigationLink(park.name, value: park)
}
.navigationDestination(for: Park.self) { park in
ParkDetails(park: park)
}
}
.instrumentNavigation(path: presentedParks) // convenience View Modifier
}
}
Whenever the path
variable changes, this View Modifier will emit a span with the name Navigation
. This span will contain the following attributes:
screen.name
(string): the full navigation path when the span is emitted. If the path passed to the view modifier is notEncodable
(ie. if you're using aNavigationPath
and have stored a value that does not conform to theCodable
protocol), then this attribute will have the value<unencodable path>
.
When using other kinds of navigation (ex. a TabView
or NavigationSplitView
), we offer a utility function Honeycomb.setCurrentScreen(path: Any)
. This will immediately emit a Navigation
span as documented above. As with the View Modifier form, if the path
is Encodable
, that will be included as an attribute on the span. Otherwise the screen.name
attribute on the span will have the value <unencodable path>
.
This function can be called from a view's onAppear
, or inside a button's action
, or wherever you decide to manage your navigation.
ex.:
struct ContentView: View {
var body: some View {
TabView {
ViewA()
.padding()
.tabItem { Label("View A") }
.onAppear {
Honeycomb.setCurrentScreen(path: "View A")
}
ViewB()
.padding()
.tabItem { Label("View B" }
.onAppear {
Honeycomb.setCurrentScreen(path: "View B")
}
ViewC()
.padding()
.tabItem { Label("View C" }
.onAppear {
Honeycomb.setCurrentScreen(path: "View C")
}
}
}
}
Regardless of which form you use, either helper will keep track of the most recent path value, and our instrumentation will sets up a SpanProcessor that will automatically propage that value as a screen.name
attribute onto any other spans.
This means that if you miss a navigation, you will see spans attributed to the wrong screen. For example:
struct ContentView: View {
var body: some View {
TabView {
ViewA()
.padding()
.tabItem { Label("View A") }
.onAppear {
Honeycomb.setCurrentScreen(path: "View A")
}
ViewB()
.padding()
.tabItem { Label("View B" }
// no onAppear callback and no reportNavigation() call
}
}
}
In this case, since View B never reports the navigation, if the user navigates to View A
and then to View B
, any spans emitted from View B
will still report screen.name: "View A"
.
Any Errors, NSErrors, or NSExceptions may be recorded as Log records using the log
method. This can be used for logging
any caught exceptions in your own code that will not be logged by our crash instrumentation.
Below is an example of logging an Error object using several custom attributes.
do {
/// ...
}
catch let error {
Honeycomb.log(
error: error,
attributes: [
"user.name": AttributeValue.string(currentUser.name),
"user.id": AttributeValue.int(currentUser.id)
],
thread: Thread.current
);
}
Argument | Type | Is Required | Description |
---|---|---|---|
error/exception | Error/NSError/NSException | true | The error or exception itself. Depending on the type of error, fields will be automatically added to the log record. |
attributes | Dictionary<string, AttributeValue> | false | Additional attributes you would like to log along with the default ones provided. |
thread | Thread | false | Thread where the error occurred. Add this to include additional attributes related to the thread |
logger | Logger | false | Defaults to the Honeycomb error Logger . Provide if you want to use a different OpenTelemetry Logger |
Set the offlineCachingEnabled
option to enable disk buffering for outgoing telemetry. This will cache your telemetry in the event of network failures and continue to retry exporting your telemetry for up to 18 hours. You will also see a minimum delay in exporting telemetry, of at least 5 seconds.
This feature is currently in alpha and may be unstable. It is currently off by default.