diff --git a/demo-app/build.gradle.kts b/demo-app/build.gradle.kts index a5b417a45..967f851fe 100644 --- a/demo-app/build.gradle.kts +++ b/demo-app/build.gradle.kts @@ -3,6 +3,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(rootLibs.plugins.androidApp) alias(libs.plugins.compose.compiler) + alias(libs.plugins.byteBuddy) } android { @@ -69,6 +70,9 @@ dependencies { implementation("io.opentelemetry.android:android-agent") //parent dir implementation("io.opentelemetry.android.instrumentation:compose-click") implementation("io.opentelemetry.android.instrumentation:sessions") + implementation("io.opentelemetry.android.instrumentation:okhttp3-library") + byteBuddy("io.opentelemetry.android.instrumentation:okhttp3-agent") + implementation(libs.okhttp) implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.activity.compose) diff --git a/demo-app/gradle/libs.versions.toml b/demo-app/gradle/libs.versions.toml index 8b4945fbc..710d3bd64 100644 --- a/demo-app/gradle/libs.versions.toml +++ b/demo-app/gradle/libs.versions.toml @@ -1,4 +1,6 @@ [versions] +byteBuddy = "1.18.8" +okhttp = "5.3.2" opentelemetry = "1.61.0" opentelemetry-alpha = "1.61.0-alpha" junit = "6.0.3" @@ -11,6 +13,7 @@ androidx-appcompat = "androidx.appcompat:appcompat:1.7.1" opentelemetry-exporter-otlp = { module = "io.opentelemetry:opentelemetry-exporter-otlp", version.ref = "opentelemetry" } opentelemetry-api-incubator = { module = "io.opentelemetry:opentelemetry-api-incubator", version.ref = "opentelemetry-alpha" } gson = "com.google.code.gson:gson:2.13.2" +okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } #Test tools androidx-junit = "androidx.test.ext:junit:1.3.0" @@ -47,3 +50,4 @@ junit = ["junit-jupiter-api", "junit-jupiter-engine", "junit-vintage-engine"] [plugins] spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +byteBuddy = { id = "net.bytebuddy.byte-buddy-gradle-plugin", version.ref = "byteBuddy" } diff --git a/demo-app/settings.gradle.kts b/demo-app/settings.gradle.kts index ccd4628fb..3e6cbc24b 100644 --- a/demo-app/settings.gradle.kts +++ b/demo-app/settings.gradle.kts @@ -28,5 +28,9 @@ includeBuild("..") { .using(project(":instrumentation:compose:click")) substitute(module("io.opentelemetry.android.instrumentation:sessions")) .using(project(":instrumentation:sessions")) + substitute(module("io.opentelemetry.android.instrumentation:okhttp3-library")) + .using(project(":instrumentation:okhttp3:library")) + substitute(module("io.opentelemetry.android.instrumentation:okhttp3-agent")) + .using(project(":instrumentation:okhttp3:agent")) } } \ No newline at end of file diff --git a/demo-app/src/main/AndroidManifest.xml b/demo-app/src/main/AndroidManifest.xml index 6795d2986..f3d8ae718 100644 --- a/demo-app/src/main/AndroidManifest.xml +++ b/demo-app/src/main/AndroidManifest.xml @@ -29,6 +29,12 @@ android:exported="true" android:label="About OpenTelemetry Android" /> + Unit) { + val context = LocalContext.current + val scope = rememberCoroutineScope() + val client = remember { OkHttpClient() } + + Scaffold( + modifier = Modifier.fillMaxSize(), + topBar = { + TopAppBar( + title = { Text("Instrumentation testing") }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon( + imageVector = Icons.Filled.ArrowBack, + contentDescription = "Back", + ) + } + }, + ) + }, + ) { innerPadding -> + Column( + modifier = + Modifier + .fillMaxSize() + .padding(innerPadding) + .verticalScroll(rememberScrollState()) + .padding(horizontal = 20.dp, vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text( + text = "OkHttp sample calls (GET, POST, delayed GET); each should produce a traced client span in your collector.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Spacer(modifier = Modifier.height(8.dp)) + OkHttpDemoActionButton( + onClick = { + scope.launch { + val msg = + runCatching { + withContext(Dispatchers.IO) { + val request = + Request.Builder().url("https://httpbin.org/status/200").get().build() + val code = client.newCall(request).execute().use { it.code } + "OkHttp: HTTP $code" + } + }.getOrElse { e -> "OkHttp 200 demo: ${e.message}" } + withContext(Dispatchers.Main) { + Toast.makeText(context, msg, Toast.LENGTH_SHORT).show() + } + } + }, + text = "GET known 200", + ) + OkHttpDemoActionButton( + onClick = { + scope.launch { + val msg = + runCatching { + withContext(Dispatchers.IO) { + val request = + Request.Builder().url("https://httpbin.org/status/404").get().build() + val code = client.newCall(request).execute().use { it.code } + "OkHttp: HTTP $code" + } + }.getOrElse { e -> "OkHttp 404 demo: ${e.message}" } + withContext(Dispatchers.Main) { + Toast.makeText(context, msg, Toast.LENGTH_SHORT).show() + } + } + }, + text = "GET known 404", + ) + OkHttpDemoActionButton( + onClick = { + scope.launch { + val msg = + runCatching { + withContext(Dispatchers.IO) { + val request = + Request.Builder().url("http://127.0.0.1:1/").get().build() + client.newCall(request).execute().use { it.code } + "OkHttp: unexpected success" + } + }.getOrElse { e -> "OkHttp failure demo: ${e.javaClass.simpleName}" } + withContext(Dispatchers.Main) { + Toast.makeText(context, msg, Toast.LENGTH_SHORT).show() + } + } + }, + text = "GET connection failure", + ) + OkHttpDemoActionButton( + onClick = { + scope.launch { + val msg = + runCatching { + withContext(Dispatchers.IO) { + val json = """{"event":"otel-android-demo","source":"okhttp"}""" + val body = + json.toRequestBody("application/json; charset=utf-8".toMediaType()) + val request = + Request.Builder().url("https://httpbin.org/post").post(body).build() + val code = client.newCall(request).execute().use { it.code } + "OkHttp POST: HTTP $code" + } + }.getOrElse { e -> "OkHttp POST demo: ${e.message}" } + withContext(Dispatchers.Main) { + Toast.makeText(context, msg, Toast.LENGTH_SHORT).show() + } + } + }, + text = "POST JSON payload", + ) + OkHttpDemoActionButton( + onClick = { + scope.launch { + val msg = + runCatching { + withContext(Dispatchers.IO) { + val request = + Request.Builder().url("https://httpbin.org/delay/3").get().build() + val code = client.newCall(request).execute().use { it.code } + "OkHttp slow GET: HTTP $code (~3s)" + } + }.getOrElse { e -> "OkHttp slow demo: ${e.message}" } + withContext(Dispatchers.Main) { + Toast.makeText(context, msg, Toast.LENGTH_SHORT).show() + } + } + }, + text = "GET slow response (3s)", + ) + } + } +} + +@Composable +private fun OkHttpDemoActionButton( + text: String, + onClick: () -> Unit, +) { + OutlinedButton( + onClick = onClick, + modifier = Modifier.fillMaxWidth().height(56.dp), + border = BorderStroke(1.dp, DemoAccentBlue), + colors = + ButtonDefaults.outlinedButtonColors( + containerColor = Color.Transparent, + contentColor = DemoAccentBlue, + ), + ) { + Text(text) + } +} diff --git a/demo-app/src/main/java/io/opentelemetry/android/demo/theme/Theme.kt b/demo-app/src/main/java/io/opentelemetry/android/demo/theme/Theme.kt index 6604f1301..3377b5029 100644 --- a/demo-app/src/main/java/io/opentelemetry/android/demo/theme/Theme.kt +++ b/demo-app/src/main/java/io/opentelemetry/android/demo/theme/Theme.kt @@ -15,9 +15,11 @@ import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView +import androidx.core.graphics.ColorUtils import androidx.core.view.WindowCompat private val DarkColorScheme = @@ -64,8 +66,13 @@ fun DemoAppTheme( if (!view.isInEditMode) { SideEffect { val window = (view.context as Activity).window - window.statusBarColor = colorScheme.primary.toArgb() - WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme + val insetsController = WindowCompat.getInsetsController(window, view) + val backgroundArgb = colorScheme.background.toArgb() + window.statusBarColor = Color.Transparent.toArgb() + window.navigationBarColor = Color.Transparent.toArgb() + val useDarkSystemIcons = ColorUtils.calculateLuminance(backgroundArgb) > 0.5 + insetsController.isAppearanceLightStatusBars = useDarkSystemIcons + insetsController.isAppearanceLightNavigationBars = useDarkSystemIcons } }