Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package io.github.openflocon.data.core.network.datasource
import androidx.paging.PagingData
import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel
import io.github.openflocon.domain.network.models.FloconNetworkCallDomainModel
import io.github.openflocon.domain.network.models.NetworkCallWithDeviceId
import io.github.openflocon.domain.network.models.NetworkFilterDomainModel
import io.github.openflocon.domain.network.models.NetworkSortDomainModel
import kotlinx.coroutines.flow.Flow
Expand Down Expand Up @@ -54,5 +55,11 @@ interface NetworkLocalDataSource {

suspend fun deleteRequestOnDifferentSession(deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel)

suspend fun getAllRequests(
deviceId: String? = null,
startTimestamp: Long? = null,
endTimestamp: Long? = null,
): List<NetworkCallWithDeviceId>

suspend fun clear()
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import io.github.openflocon.domain.messages.repository.MessagesReceiverRepositor
import io.github.openflocon.domain.network.models.BadQualityConfigDomainModel
import io.github.openflocon.domain.network.models.BadQualityConfigId
import io.github.openflocon.domain.network.models.FloconNetworkCallDomainModel
import io.github.openflocon.domain.network.models.NetworkCallWithDeviceId
import io.github.openflocon.domain.network.models.FloconNetworkResponseOnlyDomainModel
import io.github.openflocon.domain.network.models.MockNetworkDomainModel
import io.github.openflocon.domain.network.models.NetworkFilterDomainModel
Expand Down Expand Up @@ -98,6 +99,15 @@ class NetworkRepositoryImpl(
)
}

override suspend fun getAllNetworkCalls(
deviceId: String?,
startTimestamp: Long?,
endTimestamp: Long?,
): List<NetworkCallWithDeviceId> =
withContext(dispatcherProvider.data) {
networkLocalDataSource.getAllRequests(deviceId, startTimestamp, endTimestamp)
}

override suspend fun getRequests(
deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel,
ids: List<String>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,34 @@ interface FloconNetworkDao {
callId: String,
): FloconNetworkCallEntity?

@Query(
"""
SELECT * FROM FloconNetworkCallEntity
WHERE (:startTimestamp IS NULL OR request_startTime >= :startTimestamp)
AND (:endTimestamp IS NULL OR request_startTime <= :endTimestamp)
ORDER BY request_startTime ASC
""",
)
suspend fun getAllRequests(
startTimestamp: Long? = null,
endTimestamp: Long? = null,
): List<FloconNetworkCallEntity>

@Query(
"""
SELECT * FROM FloconNetworkCallEntity
WHERE deviceId = :deviceId
AND (:startTimestamp IS NULL OR request_startTime >= :startTimestamp)
AND (:endTimestamp IS NULL OR request_startTime <= :endTimestamp)
ORDER BY request_startTime ASC
""",
)
suspend fun getAllRequestsByDevice(
deviceId: String,
startTimestamp: Long? = null,
endTimestamp: Long? = null,
): List<FloconNetworkCallEntity>

@Query("DELETE FROM FloconNetworkCallEntity")
suspend fun clearAll()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import io.github.openflocon.data.local.network.mapper.toEntity
import io.github.openflocon.data.local.network.models.FloconNetworkCallEntity
import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel
import io.github.openflocon.domain.network.models.FloconNetworkCallDomainModel
import io.github.openflocon.domain.network.models.NetworkCallWithDeviceId
import io.github.openflocon.domain.network.models.NetworkFilterDomainModel
import io.github.openflocon.domain.network.models.NetworkSortDomainModel
import io.github.openflocon.domain.network.models.NetworkTextFilterColumns
Expand Down Expand Up @@ -260,6 +261,23 @@ class NetworkLocalDataSourceRoom(
)
}

override suspend fun getAllRequests(
deviceId: String?,
startTimestamp: Long?,
endTimestamp: Long?,
): List<NetworkCallWithDeviceId> {
val entities = if (deviceId != null) {
floconNetworkDao.getAllRequestsByDevice(deviceId, startTimestamp, endTimestamp)
} else {
floconNetworkDao.getAllRequests(startTimestamp, endTimestamp)
}
return entities.mapNotNull { entity ->
entity.toDomainModel()?.let { domain ->
NetworkCallWithDeviceId(deviceId = entity.deviceId, call = domain)
}
}
}

override suspend fun clear() {
floconNetworkDao.clearAll()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import org.koin.dsl.module
internal val messagesModule = module {
single<Server> {
getServer(
json = get()
json = get(),
networkRepository = lazy { get() },
)
}
singleOf(::MessageRemoteDataSourceImpl) bind MessageRemoteDataSource::class
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.flocon.data.remote.models

import kotlinx.serialization.Serializable

@Serializable
internal data class NetworkLogsExportResponse(
val data: List<NetworkCallExport>,
val metadata: ExportMetadata,
)

@Serializable
internal data class ExportMetadata(
val exportedAt: Long,
val totalItems: Int,
val filteredBy: FilterCriteria?,
)

@Serializable
internal data class FilterCriteria(
val deviceId: String? = null,
val startTimestamp: Long? = null,
val endTimestamp: Long? = null,
)

@Serializable
internal data class NetworkCallExport(
val callId: String,
val method: String,
val url: String,
val startTime: Long,
val startTimeFormatted: String,
val statusCode: Int? = null,
val durationMs: Double? = null,
val requestHeaders: Map<String, String>,
val responseHeaders: Map<String, String>? = null,
val requestBody: String? = null,
val responseBody: String? = null,
val contentType: String? = null,
val deviceId: String,
val appInstance: Long,
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.flocon.data.remote.models.FloconIncomingMessageDataModel
import com.flocon.data.remote.models.FloconOutgoingMessageDataModel
import io.github.openflocon.domain.Constant
import io.github.openflocon.domain.messages.models.FloconReceivedFileDomainModel
import io.github.openflocon.domain.network.repository.NetworkRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.json.Json
import kotlin.uuid.ExperimentalUuidApi
Expand All @@ -31,4 +32,7 @@ interface Server {
@OptIn(ExperimentalUuidApi::class)
fun newRequestId(): String = Uuid.random().toString()

expect fun getServer(json: Json): Server
expect fun getServer(
json: Json,
networkRepository: Lazy<NetworkRepository>,
): Server
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package com.flocon.data.remote.server

import co.touchlab.kermit.Logger
import com.flocon.data.remote.models.ExportMetadata
import com.flocon.data.remote.models.FilterCriteria
import com.flocon.data.remote.models.NetworkCallExport
import com.flocon.data.remote.models.NetworkLogsExportResponse
import io.github.openflocon.domain.network.models.FloconNetworkCallDomainModel
import io.github.openflocon.domain.network.models.NetworkCallWithDeviceId
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.ApplicationCall
import io.ktor.server.response.respondText
import io.ktor.server.routing.Route
import io.ktor.server.routing.get
import kotlinx.serialization.builtins.MapSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.serializer

fun Route.networkExportRoutes(
json: Json,
// Filtering is done at DB level — timestamps passed through to the query
getNetworkCalls: suspend (deviceId: String?, startTimestamp: Long?, endTimestamp: Long?) -> List<NetworkCallWithDeviceId>,
) {
// 1. GET /api/network-logs — all calls from all devices
get("/api/network-logs") {
val calls = getNetworkCalls(null, null, null)
call.respondJson(json, calls, deviceId = null, startTimestamp = null, endTimestamp = null)
}

// 2. GET /api/network-logs/{deviceId} — all calls for a specific device
get("/api/network-logs/{deviceId}") {
val deviceId = call.parameters["deviceId"]
val calls = getNetworkCalls(deviceId, null, null)
call.respondJson(json, calls, deviceId = deviceId, startTimestamp = null, endTimestamp = null)
}

// 3. GET /api/network-logs/{deviceId}/filter?startTimestamp=&endTimestamp= — device + time range
get("/api/network-logs/{deviceId}/filter") {
val deviceId = call.parameters["deviceId"]
val startTimestamp = call.request.queryParameters["startTimestamp"]?.toLongOrNull()
val endTimestamp = call.request.queryParameters["endTimestamp"]?.toLongOrNull()

val calls = getNetworkCalls(deviceId, startTimestamp, endTimestamp)
call.respondJson(json, calls, deviceId = deviceId, startTimestamp = startTimestamp, endTimestamp = endTimestamp)
}
}

private suspend fun ApplicationCall.respondJson(
json: Json,
calls: List<NetworkCallWithDeviceId>,
deviceId: String?,
startTimestamp: Long?,
endTimestamp: Long?,
) {
try {
val exportedCalls = calls.map { networkCallWithDeviceId ->
val networkCall = networkCallWithDeviceId.call
val storedDeviceId = networkCallWithDeviceId.deviceId
NetworkCallExport(
callId = networkCall.callId,
method = networkCall.request.method,
url = networkCall.request.url,
startTime = networkCall.request.startTime,
startTimeFormatted = networkCall.request.startTimeFormatted,
statusCode = when (val response = networkCall.response) {
is FloconNetworkCallDomainModel.Response.Success ->
when (val info = response.specificInfos) {
is FloconNetworkCallDomainModel.Response.Success.SpecificInfos.Http -> info.httpCode
else -> null
}
else -> null
},
durationMs = networkCall.response?.durationMs,
requestHeaders = networkCall.request.headers,
responseHeaders = when (val response = networkCall.response) {
is FloconNetworkCallDomainModel.Response.Success -> response.headers
else -> null
},
requestBody = networkCall.request.body,
responseBody = when (val response = networkCall.response) {
is FloconNetworkCallDomainModel.Response.Success -> response.body
else -> null
},
contentType = when (val response = networkCall.response) {
is FloconNetworkCallDomainModel.Response.Success -> response.contentType
else -> null
},
deviceId = storedDeviceId,
appInstance = networkCall.appInstance,
)
}

val response = NetworkLogsExportResponse(
data = exportedCalls,
metadata = ExportMetadata(
exportedAt = System.currentTimeMillis(),
totalItems = exportedCalls.size,
filteredBy = if (deviceId != null || startTimestamp != null || endTimestamp != null) {
FilterCriteria(
deviceId = deviceId,
startTimestamp = startTimestamp,
endTimestamp = endTimestamp,
)
} else null,
),
)

respondText(
json.encodeToString(NetworkLogsExportResponse.serializer(), response),
ContentType.Application.Json,
HttpStatusCode.OK,
)
} catch (e: Exception) {
Logger.e("Error exporting network logs", e)
respondText(
json.encodeToString(
MapSerializer(serializer<String>(), serializer<String>()),
mapOf("error" to (e.message ?: "Unknown error")),
),
ContentType.Application.Json,
HttpStatusCode.InternalServerError,
)
Comment thread
abdullahsohailcs marked this conversation as resolved.
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package com.flocon.data.remote.server

import io.github.openflocon.domain.network.repository.NetworkRepository
import kotlinx.serialization.json.Json

actual fun getServer(json: Json): Server = ServerJvm(json)
actual fun getServer(
json: Json,
networkRepository: Lazy<NetworkRepository>,
): Server = ServerJvm(json, networkRepository)
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.flocon.data.remote.models.FloconDeviceIdAndPackageNameDataModel
import com.flocon.data.remote.models.FloconIncomingMessageDataModel
import com.flocon.data.remote.models.FloconOutgoingMessageDataModel
import io.github.openflocon.domain.messages.models.FloconReceivedFileDomainModel
import io.github.openflocon.domain.network.repository.NetworkRepository
import io.ktor.http.content.PartData
import io.ktor.http.content.forEachPart
import io.ktor.http.content.streamProvider
Expand Down Expand Up @@ -41,6 +42,7 @@ import kotlin.time.Duration.Companion.seconds

class ServerJvm(
private val json: Json,
private val networkRepository: Lazy<NetworkRepository>,
) : Server {
private val _receivedMessages = Channel<FloconIncomingMessageDataModel>()
override val receivedMessages = _receivedMessages.receiveAsFlow()
Expand Down Expand Up @@ -251,6 +253,14 @@ class ServerJvm(

call.respondText("file received : ${savedFile?.absolutePath ?: "inconnu"}")
}

// Add network export routes
networkExportRoutes(
json = json,
getNetworkCalls = { deviceId, startTimestamp, endTimestamp ->
networkRepository.value.getAllNetworkCalls(deviceId, startTimestamp, endTimestamp)
}
)
}
}.start(wait = false)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package io.github.openflocon.domain.network.models

data class NetworkCallWithDeviceId(
val deviceId: String,
val call: FloconNetworkCallDomainModel,
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package io.github.openflocon.domain.network.repository
import androidx.paging.PagingData
import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel
import io.github.openflocon.domain.network.models.FloconNetworkCallDomainModel
import io.github.openflocon.domain.network.models.NetworkCallWithDeviceId
import io.github.openflocon.domain.network.models.NetworkFilterDomainModel
import io.github.openflocon.domain.network.models.NetworkSortDomainModel
import kotlinx.coroutines.flow.Flow
Expand Down Expand Up @@ -53,6 +54,12 @@ interface NetworkRepository {
deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel
)

suspend fun getAllNetworkCalls(
deviceId: String? = null,
startTimestamp: Long? = null,
endTimestamp: Long? = null,
): List<NetworkCallWithDeviceId>

suspend fun replayRequest(
deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel,
request: FloconNetworkCallDomainModel
Expand Down
Loading