Skip to content

Instantly share code, notes, and snippets.

@okshrey
Last active July 19, 2022 12:01
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save okshrey/fbe6349b888eba4c36e73745f9c5d2e0 to your computer and use it in GitHub Desktop.
Save okshrey/fbe6349b888eba4c36e73745f9c5d2e0 to your computer and use it in GitHub Desktop.
NetworkCallEventListener logs network performance event metrics to an AnalyticsConsumer. Read more https://medium.com/@shr8bit/how-okcredit-app-boosted-network-performance-by-30-84109080c065
package tech.okcredit.base.network
import okhttp3.*
import okhttp3.EventListener
import java.io.IOException
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.Proxy
import java.net.URL
/**
* Logs network performance event metrics to [AnalyticsConsumer]
*
* Usage - add instance of [NetworkCallEventListener] in [OkHttpClient.eventListenerFactory]
*/
class NetworkCallEventListener(
private val analyticsConsumer: AnalyticsConsumer,
) : EventListener() {
companion object {
private const val CALL_START = "callStart"
private const val PROXY_SELECT_START = "proxySelectStart"
private const val PROXY_SELECT_END = "proxySelectEnd"
private const val DNS_START = "dnsStart"
private const val DNS_END = "dnsEnd"
private const val CONNECT_START = "connectStart"
private const val SECURE_CONNECT_START = "secureConnectStart"
private const val SECURE_CONNECT_END = "secureConnectEnd"
private const val CONNECT_END = "connectEnd"
private const val CONNECT_FAILED = "connectFailed"
private const val CONNECTION_ACQUIRED = "connectionAcquired"
private const val CONNECTION_RELEASED = "connectionReleased"
private const val REQUEST_HEADERS_START = "requestHeadersStart"
private const val REQUEST_HEADERS_END = "requestHeadersEnd"
private const val REQUEST_BODY_START = "requestBodyStart"
private const val REQUEST_BODY_END = "requestBodyEnd"
private const val REQUEST_FAILED = "requestFailed"
private const val RESPONSE_HEADERS_START = "responseHeadersStart"
private const val RESPONSE_HEADERS_END = "responseHeadersEnd"
private const val RESPONSE_BODY_START = "responseBodyStart"
private const val RESPONSE_BODY_END = "responseBodyEnd"
private const val RESPONSE_FAILED = "responseFailed"
private const val CALL_END = "callEnd"
private const val CALL_FAILED = "callFailed"
private const val NETWORK_PERF = "Network Performance"
}
val eventMetrics = mutableListOf<EventMetric>()
private var startTime: Long = System.currentTimeMillis()
private var host: String? = null
private var encodedPath: String? = null
private var method: String? = null
private var requestBodyLength: Long? = null
private var responseBodyLength: Long? = null
private var wasCallSuccessful: Boolean? = null
private var statusCode: Int? = null
private var errorMessage: String? = null
private var failureReason: String? = null
private var protocol: String? = null
/**
* Adds [event] to [eventMetrics] list. If a terminal event [CALL_END] or [CALL_FAILED]
* is received, metrics are calculated and logged on [analyticsConsumer].
*/
fun consumeEvent(event: String, url: String) {
val trimmedUrl = removeParams(removeIdsFromUrl(url))
eventMetrics.add(EventMetric(event, System.currentTimeMillis() - startTime))
if (event == CALL_END || event == CALL_FAILED) {
val eventProps = mutableMapOf<String, Any>()
eventProps.setValue("URL", url)
eventProps.setValue("Filtered URL", trimmedUrl)
eventProps.setValue("Host", host)
eventProps.setValue("Path", encodedPath)
eventProps.setValue("Method", method)
eventProps.setValue("RequestBody Length", requestBodyLength)
eventProps.setValue("ResponseBody Length", responseBodyLength)
eventProps.setValue("Success", wasCallSuccessful)
eventProps.setValue("Status Code", statusCode)
eventProps.setValue("Error Message", errorMessage)
eventProps.setValue("Protocol", protocol)
val overallDuration = findTimeDifferenceBetweenEvents(CALL_START, event)
eventProps["Duration"] = overallDuration
val dnsToCallEndDuration = findTimeDifferenceBetweenEvents(DNS_START, event)
eventProps["DNS Start to Call End Duration"] = dnsToCallEndDuration
val startToDnsDuration = findTimeDifferenceBetweenEvents(CALL_START, DNS_START)
eventProps["Call Start To DNS Duration"] = startToDnsDuration
val dnsDuration = findTimeDifferenceBetweenEvents(DNS_START, DNS_END)
eventProps["DNS LookUp Duration"] = dnsDuration
val noOfDnsLookup = findOccurrenceCount(DNS_START)
eventProps["Total No of DNS LookUp"] = noOfDnsLookup
val dnsEndToConnectionStartDuration =
findTimeDifferenceBetweenEvents(DNS_END, CONNECT_START)
eventProps["DNS End to Connect Start Duration"] = dnsEndToConnectionStartDuration
val proxyDuration =
findTimeDifferenceBetweenEvents(PROXY_SELECT_START, PROXY_SELECT_END)
eventProps["Select Proxy Duration"] = proxyDuration
val connectionAcquiredDuration =
findTimeDifferenceBetweenEvents(CONNECT_START, CONNECTION_ACQUIRED)
eventProps["Connection Acquired Duration"] = connectionAcquiredDuration
val noOfConnectAttempt = findOccurrenceCount(CONNECT_START)
eventProps["Total No of Connect Attempt"] = noOfConnectAttempt
val secureConnectionDuration =
findTimeDifferenceBetweenEvents(SECURE_CONNECT_START, SECURE_CONNECT_END)
eventProps["Secure Connection Duration"] = secureConnectionDuration
val requestHeadersDuration =
findTimeDifferenceBetweenEvents(REQUEST_HEADERS_START, REQUEST_HEADERS_END)
eventProps["Request Headers Duration"] = requestHeadersDuration
val noOfRequestHeaders = findOccurrenceCount(REQUEST_HEADERS_START)
eventProps["Total No of Request Headers"] = noOfRequestHeaders
val responseHeadersDuration =
findTimeDifferenceBetweenEvents(RESPONSE_HEADERS_START, RESPONSE_HEADERS_END)
eventProps["Response Headers Duration"] = responseHeadersDuration
val responseBodyDuration =
findTimeDifferenceBetweenEvents(RESPONSE_BODY_START, RESPONSE_BODY_END)
eventProps["Response Body Duration"] = responseBodyDuration
eventProps["Request to Response Duration"] =
findTimeDifferenceBetweenRequestAndResponse()
val totalConnectedDuration =
findTimeDifferenceBetweenEvents(CONNECTION_ACQUIRED, CONNECTION_RELEASED)
eventProps["Total Connected Duration"] = totalConnectedDuration
val releasedToEndDuration =
findTimeDifferenceBetweenEvents(CONNECTION_RELEASED, CALL_END)
eventProps["Connection Released to Call End Duration"] = releasedToEndDuration
eventProps["RawEventLogs"] = getRawEventLogs()
analyticsConsumer.logMetrics(NETWORK_PERF, eventProps)
}
}
private fun getRawEventLogs(): String {
var callStartTime = 0L
val trimToTimeDiffValues = mutableListOf<Pair<String, Long>>()
eventMetrics.forEach { metric ->
if (metric.event == CALL_START && callStartTime == 0L) {
callStartTime = metric.timestamp
trimToTimeDiffValues.add(metric.event to 0L)
} else {
trimToTimeDiffValues.add(metric.event to (metric.timestamp - callStartTime))
}
}
return trimToTimeDiffValues.toString()
}
private fun findTimeDifferenceBetweenRequestAndResponse(): Long {
val startEvent = if (isRequestBodyPresent(method).not()) {
REQUEST_HEADERS_END
} else {
REQUEST_BODY_END
}
return findTimeDifferenceBetweenEvents(startEvent, RESPONSE_HEADERS_START)
}
private fun isRequestBodyPresent(method: String?): Boolean =
method == "GET" || method == "DELETE"
fun findTimeDifferenceBetweenEvents(startEvent: String, endEvent: String): Long {
var startTime: Long? = null
var endTime: Long? = null
eventMetrics.forEach { metric ->
if (metric.event == startEvent) {
startTime = metric.timestamp
} else if (metric.event == endEvent) {
endTime = metric.timestamp
}
}
if (startTime != null && endTime != null) {
return endTime!! - startTime!!
}
return -1
}
fun findOccurrenceCount(event: String): Int {
return eventMetrics.count { metric -> metric.event == event }
}
//region Callback functions
override fun callStart(call: Call) {
startTime = System.currentTimeMillis()
host = call.request().url.host
encodedPath = call.request().url.encodedPath
method = call.request().method
requestBodyLength = call.request().body?.contentLength()
consumeEvent(CALL_START, call.request().url.toString())
}
override fun proxySelectStart(call: Call, url: HttpUrl) {
consumeEvent(PROXY_SELECT_START, call.request().url.toString())
}
override fun proxySelectEnd(
call: Call,
url: HttpUrl,
proxies: List<Proxy>,
) {
consumeEvent(PROXY_SELECT_END, call.request().url.toString())
}
override fun dnsStart(call: Call, domainName: String) {
consumeEvent(DNS_START, call.request().url.toString())
}
override fun dnsEnd(
call: Call,
domainName: String,
inetAddressList: List<InetAddress>,
) {
consumeEvent(DNS_END, call.request().url.toString())
}
override fun connectStart(
call: Call,
inetSocketAddress: InetSocketAddress,
proxy: Proxy,
) {
consumeEvent(CONNECT_START, call.request().url.toString())
}
override fun secureConnectStart(call: Call) {
consumeEvent(SECURE_CONNECT_START, call.request().url.toString())
}
override fun secureConnectEnd(call: Call, handshake: Handshake?) {
consumeEvent(SECURE_CONNECT_END, call.request().url.toString())
}
override fun connectEnd(
call: Call,
inetSocketAddress: InetSocketAddress,
proxy: Proxy,
protocol: Protocol?,
) {
consumeEvent(CONNECT_END, call.request().url.toString())
}
override fun connectFailed(
call: Call,
inetSocketAddress: InetSocketAddress,
proxy: Proxy,
protocol: Protocol?,
ioe: IOException,
) {
wasCallSuccessful = false
errorMessage = ioe.message
failureReason = "Connect Failed"
consumeEvent(CONNECT_FAILED, call.request().url.toString())
}
override fun connectionAcquired(call: Call, connection: Connection) {
consumeEvent(CONNECTION_ACQUIRED, call.request().url.toString())
}
override fun connectionReleased(call: Call, connection: Connection) {
consumeEvent(CONNECTION_RELEASED, call.request().url.toString())
}
override fun requestHeadersStart(call: Call) {
consumeEvent(REQUEST_HEADERS_START, call.request().url.toString())
}
override fun requestHeadersEnd(call: Call, request: Request) {
consumeEvent(REQUEST_HEADERS_END, call.request().url.toString())
}
override fun requestBodyStart(call: Call) {
consumeEvent(REQUEST_BODY_START, call.request().url.toString())
}
override fun requestBodyEnd(call: Call, byteCount: Long) {
responseBodyLength = byteCount
consumeEvent(REQUEST_BODY_END, call.request().url.toString())
}
override fun requestFailed(call: Call, ioe: IOException) {
wasCallSuccessful = false
errorMessage = ioe.message
failureReason = "Request Failed"
consumeEvent(REQUEST_FAILED, call.request().url.toString())
}
override fun responseHeadersStart(call: Call) {
consumeEvent(RESPONSE_HEADERS_START, call.request().url.toString())
}
override fun responseHeadersEnd(call: Call, response: Response) {
protocol = response.protocol.name
statusCode = response.code
wasCallSuccessful = response.isSuccessful
consumeEvent(RESPONSE_HEADERS_END, call.request().url.toString())
}
override fun responseBodyStart(call: Call) {
consumeEvent(RESPONSE_BODY_START, call.request().url.toString())
}
override fun responseBodyEnd(call: Call, byteCount: Long) {
responseBodyLength = byteCount
consumeEvent(RESPONSE_BODY_END, call.request().url.toString())
}
override fun responseFailed(call: Call, ioe: IOException) {
wasCallSuccessful = false
errorMessage = ioe.message
failureReason = "Response Failed"
consumeEvent(RESPONSE_FAILED, call.request().url.toString())
}
override fun callEnd(call: Call) {
consumeEvent(CALL_END, call.request().url.toString())
}
override fun callFailed(call: Call, ioe: IOException) {
wasCallSuccessful = false
errorMessage = ioe.message
consumeEvent(CALL_FAILED, call.request().url.toString())
}
//endregion
//region Helper functions
fun removeIdsFromUrl(url: String): String {
val uuid = url.split("/")
.find { it.matches(Regex("[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}")) }
if (uuid?.isNotEmpty() == true) {
return removeIdsFromUrl(url.replace(uuid, "*"))
}
return url
}
fun removeParams(url: String): String {
if (URL(url).query == null) {
return url
}
return url.replace(URL(url).query, "").replace("?", "")
}
private fun <String, Any> MutableMap<String, Any>.setValue(key: String, value: Any?) {
value?.let {
this[key] = value
}
}
//endregion
data class EventMetric(val event: String, val timestamp: Long)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment