XuqmGroup-AndroidSDK/sdk-log/src/main/java/com/xuqm/sdk/log/LogQueue.kt

181 行
7.2 KiB
Kotlin

package com.xuqm.sdk.log
import android.content.Context
import android.util.Log
import com.xuqm.sdk.log.internal.LogUploader
import com.xuqm.sdk.log.internal.LogStorage
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.json.JSONArray
import org.json.JSONObject
import java.io.File
internal class LogQueue(
private val logApiUrl: String,
private val appKey: String,
private val appContext: Context,
) {
companion object {
private const val TAG = "XLog"
private const val MAX_QUEUE_SIZE = 500
private const val BATCH_SIZE = 30
private const val FLUSH_INTERVAL_MS = 10_000L
private const val PREFS_NAME = "xuqm_log_queue"
private const val KEY_ISSUES = "issues"
private const val KEY_EVENTS = "events"
}
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val pendingIssues = mutableListOf<IssueEvent>()
private val pendingEvents = mutableListOf<LogEvent>()
private val lock = Any()
init {
loadFromDisk()
scope.launch {
while (true) {
delay(FLUSH_INTERVAL_MS)
flush()
}
}
}
fun push(event: Any) {
synchronized(lock) {
when (event) {
is IssueEvent -> {
if (pendingIssues.size >= MAX_QUEUE_SIZE) pendingIssues.removeAt(0)
pendingIssues.add(event)
}
is LogEvent -> {
if (pendingEvents.size >= MAX_QUEUE_SIZE) pendingEvents.removeAt(0)
pendingEvents.add(event)
}
}
}
if (shouldFlush()) scope.launch { flush() }
}
fun uploadPendingCrashes() {
if (logApiUrl.isBlank()) return
scope.launch {
val crashDir = LogStorage.crashDir(appContext)
val files = crashDir.listFiles() ?: return@launch
for (file in files) {
runCatching {
val json = file.readText()
val obj = JSONObject(json)
val event = IssueEvent(
type = obj.optString("type", "native_crash"),
message = obj.optString("message", ""),
stack = obj.optString("stack", ""),
fingerprint = Fingerprint.compute("native_crash", obj.optString("message", ""), obj.optString("stack", "")),
appKey = obj.optString("appKey", appKey),
userId = obj.optString("userId", null),
platform = "android",
appVersion = runCatching {
appContext.packageManager.getPackageInfo(appContext.packageName, 0).versionName ?: "unknown"
}.getOrDefault("unknown"),
metadata = emptyMap(),
environment = "production",
timestamp = obj.optLong("timestamp", System.currentTimeMillis()),
)
LogUploader.uploadIssue(logApiUrl, event)
file.delete()
}.onFailure { e ->
Log.e(TAG, "Failed to upload crash: ${file.name}", e)
}
}
}
}
private fun shouldFlush(): Boolean = synchronized(lock) {
pendingIssues.size >= BATCH_SIZE || pendingEvents.size >= BATCH_SIZE
}
private fun flush() {
val issues: List<IssueEvent>
val events: List<LogEvent>
synchronized(lock) {
if (pendingIssues.isEmpty() && pendingEvents.isEmpty()) return
issues = pendingIssues.toList()
events = pendingEvents.toList()
pendingIssues.clear()
pendingEvents.clear()
}
if (issues.isNotEmpty()) {
scope.launch {
runCatching { LogUploader.uploadIssues(logApiUrl, issues) }
.onFailure { e ->
Log.e(TAG, "Failed to upload issues batch, re-queuing", e)
synchronized(lock) {
val remaining = MAX_QUEUE_SIZE - pendingIssues.size
if (remaining > 0) pendingIssues.addAll(0, issues.take(remaining))
}
}
}
}
if (events.isNotEmpty()) {
scope.launch {
runCatching { LogUploader.uploadEvents(logApiUrl, events) }
.onFailure { e ->
Log.e(TAG, "Failed to upload events batch, re-queuing", e)
synchronized(lock) {
val remaining = MAX_QUEUE_SIZE - pendingEvents.size
if (remaining > 0) pendingEvents.addAll(0, events.take(remaining))
}
}
}
}
}
private fun loadFromDisk() {
runCatching {
val prefs = appContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
prefs.getString(KEY_ISSUES, null)?.let { json ->
val arr = JSONArray(json)
synchronized(lock) {
for (i in 0 until arr.length()) {
val obj = arr.getJSONObject(i)
pendingIssues.add(IssueEvent(
type = obj.optString("type", ""),
message = obj.optString("message", ""),
stack = obj.optString("stack", ""),
fingerprint = obj.optString("fingerprint", ""),
appKey = obj.optString("appKey", appKey),
userId = obj.optString("userId", null),
platform = obj.optString("platform", "android"),
appVersion = obj.optString("appVersion", ""),
metadata = emptyMap(),
environment = obj.optString("environment", "production"),
timestamp = obj.optLong("timestamp", 0),
))
}
}
}
prefs.getString(KEY_EVENTS, null)?.let { json ->
val arr = JSONArray(json)
synchronized(lock) {
for (i in 0 until arr.length()) {
val obj = arr.getJSONObject(i)
pendingEvents.add(LogEvent(
name = obj.optString("name", ""),
appKey = obj.optString("appKey", appKey),
userId = obj.optString("userId", null),
platform = obj.optString("platform", "android"),
appVersion = obj.optString("appVersion", ""),
environment = obj.optString("environment", "production"),
timestamp = obj.optLong("timestamp", 0),
))
}
}
}
prefs.edit().remove(KEY_ISSUES).remove(KEY_EVENTS).apply()
}
}
}