Honeycomb wrapper for OpenTelemetry on Android.
STATUS: this library is in BETA. We are actively seeking feedback to ensure usability.
Honeycomb APIs and data shapes are stable and safe for production. However, this SDK depends on the OpenTelemetry Android SDK, which is still experimental and subject to change. Therefore, when updating, it is possible to encounter breaking changes in other dependencies.
These are the current versions of libraries we have tested for compatibility:
Dependency | Version |
---|---|
io.honeycomb.android:honeycomb-opentelemetry-android |
0.0.11 |
io.opentelemetry.android:core |
0.11.0-alpha |
io.opentelemetry.android:android-agent |
0.11.0-alpha |
io.opentelemetry:opentelemetry-api |
1.49.0 |
For a complete list of tested dependencies and versions, see libs.versions.toml.
Add the following dependencies to your build.gradle.kts
:
dependencies {
implementation("io.honeycomb.android:honeycomb-opentelemetry-android:0.0.11")
}
To configure the SDK in your Application class:
import io.honeycomb.opentelemetry.android.Honeycomb
import io.honeycomb.opentelemetry.android.HoneycombOptions
import io.opentelemetry.android.OpenTelemetryRum
class ExampleApp: Application() {
var otelRum: OpenTelemetryRum? = null
override fun onCreate() {
super.onCreate()
val options = HoneycombOptions.builder(this)
.setApiKey("YOUR-API-KEY")
.setServiceName("YOUR-SERVICE-NAME")
.build()
otelRum = Honeycomb.configure(this, options)
}
}
To manually send a span:
val app = application as ExampleApp
val otelRum = app.otelRum
val otel = otelRum?.openTelemetryA
val tracer = otel?.getTracer("YOUR-INSTRUMENTATION-NAME")
val span = tracer?.spanBuilder("YOUR-SPAN-NAME")?.startSpan()
span?.end()
Option | Type | Required? | Description |
---|---|---|---|
apiKey |
String | Yes | Default Honeycomb API Key to use to send data directly to Honeycomb. |
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 . |
apiEndpoint |
String | No | API endpoint to send data to. |
tracesEndpoint |
String | No | API endpoint to send traces to. Overrides apiEndpoint for trace data. |
metricsEndpoint |
String | No | API endpoint to send metrics to. Overrides apiEndpoint for metrics data. |
logsEndpoint |
String | No | API endpoint to send trace to. Overrides apiEndpoint for logs data. |
spanProcessor |
io.opentelemetry.sdk.trace.SpanProcessor | No | Additional span processor to use. |
logRecordProcessor |
io.opentelemetry.sdk.logs.LogRecordProcessor | No | Log Record processor to use. |
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 |
io.honeycomb.opentelemetry.android.OtlpProtocol | No | Protocol to use when sending data. |
tracesProtocol |
io.honeycomb.opentelemetry.android.OtlpProtocol | No | Overrides protocol for trace data. |
metricsProtocol |
io.honeycomb.opentelemetry.android.OtlpProtocol | No | Overrides protocol for metrics data. |
logsProtocol |
io.honeycomb.opentelemetry.android.OtlpProtocol | No | Overrides protocol for logs data. |
offlineCachingEnabled |
Boolean | 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 telemetry emitted will have the following resource attributes attached:
device.manufacturer
: Manufacturer of the device, as reported byandroid.os.Build.MANUFACTURER
device.model.identifier
: Model of the device, as reported byandroid.os.Build.MODEL
device.model.name
: seedevice.model.identifier
honeycomb.distro.runtime_version
: Version of Android on the device. See alsoos.version
.honeycomb.distro.version
: Version of the Honeycomb SDK being used.os.description
: String containing Android version, build ID, and SDK level.os.name
: "android"os.type
: "linux"os.version
: The value ofandroid.os.Build.VERSION.RELEASE
rum.sdk.version
: Version of the OpenTelemetry Android SDK being used.screen.name
: Name of the current Activity or Fragment (see Navigation section below)service.name
: The name of your application, as provided viasetServiceName()
, orunknown_service
if unset.service.version
: Optional. The version of your application, as provided via `setServiceVersion()telemetry.distro.version
: Version of the Honeycomb SDK being used.telemetry.distro.name
: "io.honeycomb.opentelemetry.android"telemetry.sdk.language
: "android"telemetry.sdk.name
: "opentelemetry"telemetry.sdk.version
: version of the base OpenTelemetry SDK being used
To enable all OpenTelemetry auto-instrumentation, simply include android-agent
as a dependency:
If you want to pick and choose which auto-instrumentation to include, you can instead add dependencies for whichever components you would like:
io.opentelemetry.android:instrumentation-activity
io.opentelemetry.android:instrumentation-anr
io.opentelemetry.android:instrumentation-crash
io.opentelemetry.android:instrumentation-fragment
io.opentelemetry.android:instrumentation-slowrendering
The following additional auto-instrumentation is implemented in this library:
honeycomb-opentelemetry-android-interaction
— UI interaction in XML-based Activities.
A trace is emitted before, during, and after each stage of the Android Activity Lifecycle.
- AppStart
- name:
AppStart
- attributes:
start.type
:"hot"
or"cold"
- name:
- Created
- name:
Created
- events:
activityPreCreated
,activityCreated
,activityPostCreated
activityPreStarted
,activityStarted
,activityPostStarted
activityPreResumed
,activityResumed
,activityPostResumed
- name:
- Paused
- name:
Paused
- events:
activityPrePaused
,activityPaused
,activityPostPaused
- name:
- Stopped
- name:
Stopped
- events:
activityPreStopped
,activityStopped
,activityPostStopped
- name:
- Destroyed
- name:
Destroyed
- events:
activityPreDestroyed
,activityDestroyed
,activityPostDestroyed
- name:
activityName
- The unqualified name for the activity, such as"MainActivity"
A trace is emitted whenever the app becomes unresponsive for longer than approximately 5 seconds (ANR).
- name:
ANR
- attributes:
exception.stacktrace
A trace is emitted whenever an uncaught exception is going to terminate the program. This is in addition to whatever other crash reporting is enabled. These traces are flushed as the app is shutting down, so the app does not need to be re-opened for them to be sent.
- name:
UncaughtException
- attributes:
exception.name
exception.message
exception.stacktrace
exception.escaped
thread.name
thread.id
A trace is emitted whenever a frame is rendered too slowly, according to Android's Slow rendering guidelines.
- name:
slowRenders
(>16ms) orfrozenRenders
(>700ms) - attributes:
activity.name
- the fully-qualified name of the activity, such as"io.honeycomb.opentelemetry.android.example/.MainActivity"
Network instrumentation is not included by default but can be configured on top of this distro. We test with the following OpenTelemetry instrumentation packages:
Configuration of each of these packages is described in their respective READMEs.
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, using okhttp.
private suspend fun doRequest(request: Request): String {
val client = OkHttpClient.Builder().build()
val result: Result<String> = suspendCoroutine { cont ->
val callback = object : Callback {
override fun onFailure(call: Call, e: IOException) {
// handle failure
}
override fun onResponse(call: Call, response: Response) {
// handle successful request
}
}
// inject otel context onto the request headers
val requestWithHeaders = request.newBuilder()
otelRum?.openTelemetry?.propagators?.textMapPropagator?.inject(
OtelContext.current(),
builder,
TEXT_MAP_SETTER
).build()
client.newCall(requestWithHeaders).enqueue(callback)
}
return result.getOrThrow()
}
Various touch events are instrumented for TextView
fields, such as:
Touch Began
- A touch startedTouch Ended
- A touch endedclick
- A "tap"
These events may have the following attributes.
view.class
- e.g."android.widget.Button"
view.accessibilityClassName
- TheaccessibilityClassName
for the view. Usually the same asview.class
.view.text
- The text of theTextView
.view.id
- The XML ID of the view element, as an integer.view.id.entry
- The text form of the XML ID of the view element.view.id.package
- The package for the XML ID of the view.view.name
- The "best" available name of the view, given the other identifiers. Usually the same asview.id.entry
.
If you have included the activity or fragment auto-instrumentation, that library will track navigation between Activities and Fragments in your Android application. It automatically sets the screen.name
attribute on spans to the name of the current Activity or Fragment that a user is viewing. This provides visibility into user navigation patterns and helps correlate other telemetry with the specific screen where events occurred.
In most cases, OpenTelemetry context will be propagated correctly across function calls, etc. By default, OpenTelemetry context is stored in a java.lang.ThreadLocal
. However, Kotlin Coroutines may operate across multiple threads, and therefore do not automatically inherit the correct OpenTelemetry context.
Instead, context must be propagated manually, via the OpenTelemetry Kotlin Extensions.
dependencies {
implementation("io.opentelemetry:opentelemetry-extension-kotlin:1.47.0")
}
Once these are installed, replace any launch
calls with
launch(Span.current().asContextElement()) {
}
Exceptions may be recorded as Log records using the logException
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 Exception object using several custom attributes.
try {
// ...
} catch (e: Exception) {
Honeycomb.logException(
otel,
e,
Attributes.of(
AttributeKey.stringKey("user.name"), "bufo",
AttributeKey.longKey("user.id"), 1
),
Thread.currentThread())
}
Argument | Type | Is Required | Description |
---|---|---|---|
otel | OpenTelemetryRum | true | The OpenTelemetryRum instance to use for logging. |
exception | Throwable | true | The exception itself. Attributes will be automatically added to the log record. |
attributes | Attributes? | 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 the thread as attributes. |
Android Compose instrumentation is included in a standalone library. Add the following to your dependencies in build.gradle.kts
:
dependencies {
implementation("io.honeycomb.android:honeycomb-opentelemetry-android-compose:0.0.11")
}
After you initialize the Honeycomb
sdk, wrap your entire app in a CompositionLocalProvider
that provides LocalOpenTelemetryRum
, as so:
import io.honeycomb.opentelemetry.android.compose.LocalOpenTelemetryRum
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val app = application as ExampleApp
val otelRum = app.otelRum
setContent {
CompositionLocalProvider(LocalOpenTelemetryRum provides otelRum) {
// app content
}
}
}
}
Wrap your Composables with HoneycombInstrumentedComposable(name: String)
, like so:
@Composable
private fun MyComposable() {
HoneycombInstrumentedComposable("main view") {
// ...
}
}
This will measure and emit instrumentation for your Composable's render times, ex:
Specifically, it will emit 2 kinds of span for each composable 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 toHoneycombInstrumentedComposable
view.renderDuration
(double): amount of time in seconds to spent initializing the contents ofHoneycombInstrumentedComposable
view.totalDuration
(double): amount of time in seconds from when the contents ofHoneycombInstrumentedComposable
start initializing to when the contents appear on screen
View Body
spans encompass just the contents of the HoneycombInstrumentedView
, and include the following attributes:
view.name
(string): the name passed toHoneycombInstrumentedComposable
You can implement and register your own custom span processor with the Honeycomb SDK. This allows you to perform custom operations on spans before they are exported, such as adding application-specific attributes.
import io.opentelemetry.context.Context
import io.opentelemetry.sdk.common.CompletableResultCode
import io.opentelemetry.sdk.trace.ReadWriteSpan
import io.opentelemetry.sdk.trace.ReadableSpan
import io.opentelemetry.sdk.trace.SpanProcessor
// Create a custom span processor
class MyCustomSpanProcessor : SpanProcessor {
override fun onStart(parentContext: Context, span: ReadWriteSpan) {
// Add custom logic when a span starts
// For example, add a custom attribute to every span:
span.setAttribute("custom.attribute", "custom_value")
}
override fun onEnd(span: ReadableSpan) {
// Add custom logic when a span ends
}
override fun shutdown(): CompletableResultCode {
// Clean up any resources
return CompletableResultCode.ofSuccess()
}
override fun isStartRequired(): Boolean = true
override fun isEndRequired(): Boolean = true
}
// Then when configuring the SDK, add your processor:
val options = HoneycombOptions.builder(this)
.setApiKey("YOUR-API-KEY")
.setServiceName("YOUR-SERVICE-NAME")
.setSpanProcessor(MyCustomSpanProcessor())
.build()
otelRum = Honeycomb.configure(this, options)
To use multiple custom span processors, you can combine them using OpenTelemetry's SpanProcessor.composite()
helper:
import io.opentelemetry.context.Context
import io.opentelemetry.sdk.common.CompletableResultCode
import io.opentelemetry.sdk.trace.ReadWriteSpan
import io.opentelemetry.sdk.trace.ReadableSpan
import io.opentelemetry.sdk.trace.SpanProcessor
// Combine multiple span processors
val combinedProcessor = SpanProcessor.composite(
FirstSpanProcessor(),
SecondSpanProcessor(),
)
// Add the combined processor to your options
val options = HoneycombOptions.builder(this)
.setApiKey("YOUR-API-KEY")
.setServiceName("YOUR-SERVICE-NAME")
.setSpanProcessor(combinedProcessor)
.build()
otelRum = Honeycomb.configure(this, options)
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 30 seconds.
This feature is currently in alpha and may be unstable. It is currently off by default.