Android
Android OpenTelemetry SDK¶
This guide covers how to instrument Android applications with OpenTelemetry to send traces to Sematext Tracing.
Instrumentation Approach¶
Android supports both auto and manual instrumentation:
- Auto-instrumentation: The OpenTelemetry Android agent automatically instruments popular libraries like OkHttp, Retrofit, and Room database without code changes
- Manual instrumentation: Add custom spans for business logic, user interactions, and specific application flows
Most Android applications benefit from combining both approaches - auto-instrumentation for standard libraries and manual instrumentation for custom business logic.
Prerequisites¶
- A Sematext Tracing App (create one here) and its token
- Android API level 21+ (Android 5.0 Lollipop or higher)
- Gradle 7.0 or higher
- Kotlin 1.6+ or Java 8+
Installation¶
1. Add Dependencies¶
Add the following to your app's build.gradle (or build.gradle.kts for Kotlin DSL):
Groovy (build.gradle):
dependencies {
implementation 'io.opentelemetry:opentelemetry-api:1.32.0'
implementation 'io.opentelemetry:opentelemetry-sdk:1.32.0'
implementation 'io.opentelemetry:opentelemetry-exporter-otlp:1.32.0'
implementation 'io.opentelemetry.android:android-agent:0.4.0'
implementation 'io.opentelemetry.instrumentation:opentelemetry-okhttp-3.0:1.32.0-alpha'
}
Kotlin DSL (build.gradle.kts):
dependencies {
implementation("io.opentelemetry:opentelemetry-api:1.32.0")
implementation("io.opentelemetry:opentelemetry-sdk:1.32.0")
implementation("io.opentelemetry:opentelemetry-exporter-otlp:1.32.0")
implementation("io.opentelemetry.android:android-agent:0.4.0")
implementation("io.opentelemetry.instrumentation:opentelemetry-okhttp-3.0:1.32.0-alpha")
}
2. Network Permissions¶
Add the following permission to your AndroidManifest.xml:
Basic Configuration¶
1. Initialize OpenTelemetry in Your Application Class¶
Create or update your Application class:
Kotlin:
import android.app.Application
import io.opentelemetry.android.OpenTelemetryRum
import io.opentelemetry.android.config.OtelRumConfig
import io.opentelemetry.api.common.Attributes
import io.opentelemetry.api.trace.Tracer
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter
import io.opentelemetry.sdk.OpenTelemetrySdk
import io.opentelemetry.sdk.resources.Resource
import io.opentelemetry.sdk.trace.SdkTracerProvider
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor
import io.opentelemetry.semconv.resource.attributes.ResourceAttributes
class MyApplication : Application() {
companion object {
lateinit var tracer: Tracer
private set
}
override fun onCreate() {
super.onCreate()
initializeOpenTelemetry()
}
private fun initializeOpenTelemetry() {
// Configure resource with service information
val resource = Resource.getDefault()
.merge(Resource.create(
Attributes.builder()
.put(ResourceAttributes.SERVICE_NAME, "my-android-app")
.put(ResourceAttributes.SERVICE_VERSION, BuildConfig.VERSION_NAME)
.put("environment", if (BuildConfig.DEBUG) "development" else "production")
.build()
))
// Ship traces directly to the Sematext managed OTLP receiver.
// US gRPC endpoint shown; for EU use https://otlp-receiver-grpc.eu.sematext.com:443
val spanExporter = OtlpGrpcSpanExporter.builder()
.setEndpoint("https://otlp-receiver-grpc.sematext.com:443")
.addHeader("Authorization", "Bearer your-api-key")
.build()
// Build the tracer provider
val tracerProvider = SdkTracerProvider.builder()
.setResource(resource)
.addSpanProcessor(BatchSpanProcessor.builder(spanExporter).build())
.build()
// Initialize OpenTelemetry SDK
val openTelemetry = OpenTelemetrySdk.builder()
.setTracerProvider(tracerProvider)
.buildAndRegisterGlobal()
// Get tracer for application use
tracer = openTelemetry.getTracer("my-android-app")
// Optional: Initialize RUM (Real User Monitoring) features
val rumConfig = OtelRumConfig()
.setGlobalAttributes(
Attributes.builder()
.put("app.name", "my-android-app")
.put("app.version", BuildConfig.VERSION_NAME)
.build()
)
OpenTelemetryRum.builder(this, rumConfig)
.setOpenTelemetry(openTelemetry)
.build()
}
}
Java:
import android.app.Application;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
import io.opentelemetry.semconv.resource.attributes.ResourceAttributes;
public class MyApplication extends Application {
private static Tracer tracer;
public static Tracer getTracer() {
return tracer;
}
@Override
public void onCreate() {
super.onCreate();
initializeOpenTelemetry();
}
private void initializeOpenTelemetry() {
// Configure resource
Resource resource = Resource.getDefault()
.merge(Resource.create(
Attributes.builder()
.put(ResourceAttributes.SERVICE_NAME, "my-android-app")
.put(ResourceAttributes.SERVICE_VERSION, BuildConfig.VERSION_NAME)
.put("environment", BuildConfig.DEBUG ? "development" : "production")
.build()
));
// Ship traces directly to the Sematext managed OTLP receiver.
// US gRPC endpoint shown; for EU use https://otlp-receiver-grpc.eu.sematext.com:443
OtlpGrpcSpanExporter spanExporter = OtlpGrpcSpanExporter.builder()
.setEndpoint("https://otlp-receiver-grpc.sematext.com:443")
.addHeader("Authorization", "Bearer your-api-key")
.build();
// Build tracer provider
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
.setResource(resource)
.addSpanProcessor(BatchSpanProcessor.builder(spanExporter).build())
.build();
// Initialize SDK
OpenTelemetrySdk openTelemetry = OpenTelemetrySdk.builder()
.setTracerProvider(tracerProvider)
.buildAndRegisterGlobal();
tracer = openTelemetry.getTracer("my-android-app");
}
}
2. Update AndroidManifest.xml¶
Register your custom Application class:
<application
android:name=".MyApplication"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
...>
<!-- Your activities and other components -->
</application>
What's Automatically Instrumented¶
Once configured, the OpenTelemetry Android agent automatically traces:
- OkHttp: All HTTP requests and responses
- Retrofit: REST API calls built on OkHttp
- Room Database: Database queries and transactions
- Android Activity: Basic activity lifecycle events
- Android Fragment: Fragment lifecycle events
These automatic traces appear without any additional code. You only need manual instrumentation for custom business logic and specific user interactions.
Manual Instrumentation Examples¶
While auto-instrumentation handles standard libraries automatically, you'll need manual instrumentation for custom business logic, UI interactions, and specific application flows. Here are common patterns:
Activity Lifecycle Tracing (Manual)¶
Kotlin:
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import io.opentelemetry.api.trace.Span
import io.opentelemetry.context.Scope
class MainActivity : AppCompatActivity() {
private var activitySpan: Span? = null
private var scope: Scope? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Start span for activity creation
activitySpan = MyApplication.tracer
.spanBuilder("MainActivity.onCreate")
.startSpan()
scope = activitySpan?.makeCurrent()
try {
setContentView(R.layout.activity_main)
// Your initialization code
activitySpan?.setAttribute("screen.name", "main")
activitySpan?.setAttribute("user.id", getUserId())
} catch (e: Exception) {
activitySpan?.recordException(e)
throw e
}
}
override fun onResume() {
super.onResume()
val span = MyApplication.tracer
.spanBuilder("MainActivity.onResume")
.startSpan()
try {
// Your resume logic
} finally {
span.end()
}
}
override fun onDestroy() {
super.onDestroy()
scope?.close()
activitySpan?.end()
}
}
Network Request Tracing¶
OkHttp is automatically instrumented, but you can add custom spans around network calls for additional context:
import okhttp3.OkHttpClient
import io.opentelemetry.instrumentation.okhttp.v3_0.OkHttpTelemetry
class NetworkService {
private val client: OkHttpClient = OkHttpTelemetry.builder(
MyApplication.tracer.tracerProvider
).build().newCallFactory(OkHttpClient())
suspend fun fetchData(url: String): String {
val request = Request.Builder()
.url(url)
.build()
val span = MyApplication.tracer
.spanBuilder("fetch-data")
.setAttribute("http.url", url)
.startSpan()
return try {
val response = client.newCall(request).execute()
span.setAttribute("http.status_code", response.code.toLong())
response.body?.string() ?: ""
} catch (e: Exception) {
span.recordException(e)
throw e
} finally {
span.end()
}
}
}
User Interaction Tracing (Manual)¶
class UserInteractionTracker {
fun trackButtonClick(buttonName: String) {
val span = MyApplication.tracer
.spanBuilder("button-click")
.setAttribute("button.name", buttonName)
.setAttribute("screen.name", getCurrentScreen())
.startSpan()
try {
// Process click
} finally {
span.end()
}
}
fun trackUserAction(action: String, attributes: Map<String, String>) {
val span = MyApplication.tracer
.spanBuilder("user-action")
.setAttribute("action.type", action)
.startSpan()
attributes.forEach { (key, value) ->
span.setAttribute(key, value)
}
span.end()
}
}
Sending Traces to Sematext¶
Android apps ship spans straight to the Sematext managed OTLP endpoint over HTTPS. Authenticate with your Tracing App token (shown in the exporter config above), and pick the endpoint for your account region:
| Region | gRPC endpoint | HTTP endpoint |
|---|---|---|
| US | https://otlp-receiver-grpc.sematext.com:443 |
https://otlp-receiver.sematext.com |
| EU | https://otlp-receiver-grpc.eu.sematext.com:443 |
https://otlp-receiver.eu.sematext.com |
Region matters
Tokens are region-bound. A US-region token sent to the EU endpoint (or vice versa) silently drops data. Match the endpoint to the region of the Sematext Cloud account that owns your App.
Performance Considerations¶
Sampling¶
Configure sampling to reduce overhead:
import io.opentelemetry.sdk.trace.samplers.Sampler
val tracerProvider = SdkTracerProvider.builder()
.setSampler(Sampler.traceIdRatioBased(0.1)) // Sample 10% of traces
.build()
Batch Processing¶
Configure batch processing for efficient network usage:
val batchProcessor = BatchSpanProcessor.builder(spanExporter)
.setScheduleDelay(5000, TimeUnit.MILLISECONDS) // Send every 5 seconds
.setMaxQueueSize(2048) // Maximum spans to queue
.setMaxExportBatchSize(512) // Maximum batch size
.build()
Background vs Foreground¶
Adjust tracing based on app state:
class AppLifecycleObserver : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
// App in foreground - enable full tracing
updateSamplingRate(0.1)
}
override fun onStop(owner: LifecycleOwner) {
// App in background - reduce tracing
updateSamplingRate(0.01)
}
}
Best Practices¶
Privacy and Security¶
- Never include sensitive user data in traces (passwords, tokens, PII)
- Use HTTPS for all trace exports
- Consider data residency requirements
Mobile-Specific Attributes¶
Add mobile-specific context to your traces:
span.setAttribute("device.manufacturer", Build.MANUFACTURER)
span.setAttribute("device.model", Build.MODEL)
span.setAttribute("os.version", Build.VERSION.RELEASE)
span.setAttribute("app.version", BuildConfig.VERSION_NAME)
span.setAttribute("network.type", getNetworkType())
span.setAttribute("battery.level", getBatteryLevel())
Error Handling¶
try {
// Your operation
} catch (e: Exception) {
span.recordException(e)
span.setStatus(StatusCode.ERROR, e.message ?: "Unknown error")
// Handle error appropriately
} finally {
span.end()
}
Troubleshooting¶
Common Issues¶
No Traces Appearing:
- Confirm the OTLP endpoint region matches your account (US vs EU)
- Verify the auth header carries a valid Tracing App token
- Check network permissions in AndroidManifest.xml
- Ensure the Application class is registered
High Battery Usage:
- Reduce sampling rate
- Increase batch delay
- Disable tracing in background
Memory Issues:
- Reduce max queue size
- Implement proper span lifecycle management
- Use try-finally blocks to ensure spans are ended
Debug Logging¶
Enable debug logging for troubleshooting:
// Add to your Application class
if (BuildConfig.DEBUG) {
OpenTelemetry.setGlobalLoggerProvider(
LoggerProvider { name ->
object : Logger {
override fun log(level: Level, message: String) {
Log.d("OTel-$name", message)
}
}
}
)
}