Honeycomb wrapper for OpenTelemetry on iOS and macOS.
STATUS: this library is EXPERIMENTAL. Data shapes are unstable and not 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.1-alpha")
],
- 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()
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"
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"
.