Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion android-test-app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ android {

dependencies {
implementation(libs.playservices.safetynet)
implementation(projects.okhttp)
"friendsImplementation"(projects.okhttp)
implementation(libs.androidx.activity)

androidTestImplementation(libs.androidx.junit)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,21 @@ import androidx.activity.ComponentActivity
import okhttp3.Call
import okhttp3.Callback
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.OkHttpClient.Builder
import okhttp3.Request
import okhttp3.Response
import okhttp3.android.addTracing
import okhttp3.internal.platform.AndroidPlatform
import okio.IOException

open class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val client = OkHttpClient()
val client =
Builder()
.addTracing()
.build()

// Ensure we are compiling against the right variant
println(AndroidPlatform.isSupported)
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ androidx-annotation = "1.9.1"
androidx-espresso-core = "3.7.0"
androidx-junit = "1.3.0"
androidx-test-runner = "1.7.0"
androidx-tracing = "1.3.0"
animalsniffer = "2.0.1"
animalsniffer-annotations = "1.27"
assertk = "0.28.1"
Expand Down Expand Up @@ -75,6 +76,7 @@ androidx-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-j
androidx-lint-gradle = { module = "androidx.lint:lint-gradle", version.ref = "lint-gradle" }
androidx-startup-runtime = { module = "androidx.startup:startup-runtime", version.ref = "startup-runtime" }
androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test-runner" }
androidx-tracing-ktx = { module = "androidx.tracing:tracing-ktx", version.ref = "androidx-tracing" }
animalsniffer-annotations = { module = "org.codehaus.mojo:animal-sniffer-annotations", version.ref = "animalsniffer-annotations" }
aqute-bnd = { module = "biz.aQute.bnd:biz.aQute.bnd", version.ref = "bnd" }
aqute-resolve = { module = "biz.aQute.bnd:biz.aQute.resolve", version.ref = "bnd" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,13 @@ internal open class RecordingConnectionListener(
}

override fun connectStart(
connectionId: Long,
route: Route,
call: Call,
) = logEvent(ConnectionEvent.ConnectStart(System.nanoTime(), route, call))

override fun connectFailed(
connectionId: Long,
route: Route,
call: Call,
failure: IOException,
Expand Down
1 change: 1 addition & 0 deletions okhttp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ kotlin {
compileOnly(libs.conscrypt.openjdk)
implementation(libs.androidx.annotation)
implementation(libs.androidx.startup.runtime)
implementation(libs.androidx.tracing.ktx)
}
}

Expand Down
34 changes: 34 additions & 0 deletions okhttp/src/androidMain/kotlin/okhttp3/android/Tracing.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright (c) 2026 OkHttp Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.android

import okhttp3.ConnectionPool
import okhttp3.OkHttpClient
import okhttp3.internal.tracing.AndroidxTracingConnectionListener
import okhttp3.internal.tracing.AndroidxTracingInterceptor

/**
* Configures the [OkHttpClient] with AndroidX Tracing support.
*
* This enables visibility into network requests and connection lifecycle events
* within Android System Tracing (Perfetto/systrace). It installs a custom
* [ConnectionPool] and a network interceptor to capture tracing spans.
*
* @return This builder for method chaining.
*/
fun OkHttpClient.Builder.addTracing(): OkHttpClient.Builder =
this.connectionPool(ConnectionPool(connectionListener = AndroidxTracingConnectionListener()))
.addNetworkInterceptor(AndroidxTracingInterceptor())
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* Copyright (c) 2026 OkHttp Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress(
"CANNOT_OVERRIDE_INVISIBLE_MEMBER",
"INVISIBLE_MEMBER",
"INVISIBLE_REFERENCE",
)

package okhttp3.internal.tracing

import androidx.tracing.Trace
import okhttp3.Call
import okhttp3.Connection
import okhttp3.Route
import okhttp3.internal.connection.ConnectionListener
import okio.IOException

/**
* Tracing implementation of ConnectionListener that marks the lifetime of each connection
* in Perfetto traces.
*/
internal class AndroidxTracingConnectionListener(
private val delegate: ConnectionListener = NONE,
val traceLabel: (Route) -> String = { it.defaultTracingLabel },
) : ConnectionListener() {
override fun connectStart(
connectionId: Long,
route: Route,
call: Call,
) {
Trace.beginAsyncSection(labelForTrace(route), connectionId.toInt())
delegate.connectStart(connectionId, route, call)
}

override fun connectFailed(
connectionId: Long,
route: Route,
call: Call,
failure: IOException,
) {
Trace.endAsyncSection(labelForTrace(route), connectionId.toInt())
delegate.connectFailed(connectionId, route, call, failure)
}

override fun connectEnd(
connection: Connection,
route: Route,
call: Call,
) {
delegate.connectEnd(connection, route, call)
}

override fun connectionClosed(connection: Connection) {
Trace.endAsyncSection(labelForTrace(connection.route()), connection.id.toInt())
delegate.connectionClosed(connection)
}

private fun labelForTrace(route: Route): String = traceLabel(route).take(AndroidxTracingInterceptor.Companion.MAX_TRACE_LABEL_LENGTH)

override fun connectionAcquired(
connection: Connection,
call: Call,
) {
delegate.connectionAcquired(connection, call)
}

override fun connectionReleased(
connection: Connection,
call: Call,
) {
delegate.connectionReleased(connection, call)
}

override fun noNewExchanges(connection: Connection) {
delegate.noNewExchanges(connection)
}

companion object {
val Route.defaultTracingLabel: String
get() = this.address.url.host
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package okhttp3.internal.tracing

import androidx.tracing.trace
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response

/**
* Tracing implementation of Interceptor that marks each Call in a Perfetto
* trace. Typically used as a network interceptor.
*/
internal class AndroidxTracingInterceptor(
val traceLabel: (Request) -> String = { it.defaultTracingLabel },
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response =
trace(traceLabel(chain.request()).take(MAX_TRACE_LABEL_LENGTH)) {
chain.proceed(chain.request())
}

companion object {
internal const val MAX_TRACE_LABEL_LENGTH = 127

val Request.defaultTracingLabel: String
get() {
return url.encodedPath
}
}
}
4 changes: 4 additions & 0 deletions okhttp/src/commonJvmAndroid/kotlin/okhttp3/Connection.kt
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ import java.net.Socket
* been found. But only complete the stream once its data stream has been exhausted.
*/
interface Connection {
/** Unique id of this connection, assigned at the time of the attempt. */
val id: Long
get() = 0L

/** Returns the route used by this connection. */
fun route(): Route

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import java.net.Socket as JavaNetSocket
import java.net.UnknownServiceException
import java.security.cert.X509Certificate
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicLong
import javax.net.ssl.SSLPeerUnverifiedException
import javax.net.ssl.SSLSocket
import okhttp3.CertificatePinner
Expand Down Expand Up @@ -74,6 +75,8 @@ class ConnectPlan internal constructor(
internal val isTlsFallback: Boolean,
) : RoutePlanner.Plan,
ExchangeCodec.Carrier {
private val id = idGenerator.incrementAndGet()

/** True if this connect was canceled; typically because it lost a race. */
@Volatile private var canceled = false

Expand Down Expand Up @@ -130,7 +133,7 @@ class ConnectPlan internal constructor(
call.plansToCancel += this
try {
call.eventListener.connectStart(call, route.socketAddress, route.proxy)
connectionPool.connectionListener.connectStart(route, call)
connectionPool.connectionListener.connectStart(id, route, call)

connectSocket()
success = true
Expand All @@ -145,7 +148,7 @@ class ConnectPlan internal constructor(
)
}
call.eventListener.connectFailed(call, route.socketAddress, route.proxy, null, e)
connectionPool.connectionListener.connectFailed(route, call, e)
connectionPool.connectionListener.connectFailed(id, route, call, e)
return ConnectResult(plan = this, throwable = e)
} finally {
call.plansToCancel -= this
Expand Down Expand Up @@ -236,7 +239,7 @@ class ConnectPlan internal constructor(
return ConnectResult(plan = this)
} catch (e: IOException) {
call.eventListener.connectFailed(call, route.socketAddress, route.proxy, null, e)
connectionPool.connectionListener.connectFailed(route, call, e)
connectionPool.connectionListener.connectFailed(id, route, call, e)

if (!retryOnConnectionFailure || !retryTlsHandshake(e)) {
retryTlsConnection = null
Expand Down Expand Up @@ -330,7 +333,7 @@ class ConnectPlan internal constructor(
"Too many tunnel connections attempted: $MAX_TUNNEL_ATTEMPTS",
)
call.eventListener.connectFailed(call, route.socketAddress, route.proxy, null, failure)
connectionPool.connectionListener.connectFailed(route, call, failure)
connectionPool.connectionListener.connectFailed(id, route, call, failure)
return ConnectResult(plan = this, throwable = failure)
}
}
Expand Down Expand Up @@ -564,5 +567,6 @@ class ConnectPlan internal constructor(
companion object {
private const val NPE_THROW_WITH_NULL = "throw with null exception"
private const val MAX_TUNNEL_ATTEMPTS = 21
private val idGenerator = AtomicLong(0)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ internal abstract class ConnectionListener {
* Invoked as soon as a call causes a connection to be started.
*/
open fun connectStart(
connectionId: Long,
route: Route,
call: Call,
) {}
Expand All @@ -40,6 +41,7 @@ internal abstract class ConnectionListener {
* Invoked when a connection fails to be established.
*/
open fun connectFailed(
connectionId: Long,
route: Route,
call: Call,
failure: IOException,
Expand Down
Loading