Skip to content

Commit

Permalink
App lifecycle logging & log persistence (#322)
Browse files Browse the repository at this point in the history
* implemented force quit logger

* cleanup

* remove magic number

* Update build.gradle

* Update build.gradle

* Store and retrieve the last force quit timestamp in shared preferences before checking for force quits in Radar.kt

* first draft

* passing unit test and manual tests

* cleanup

* add in-code documentation

* allow custom edting of timestamp

* changed android into individual files

* format

* added feature flag

* fix test

* bump version

* add dynamic updated feature flag

* fixed flag setter

* bump beta version

* make logging backgrounding and resign active safe if not init

* fixed tests

* test-cleanup

* bump-beta

* cleanup

* restart ci

* addressed code review

* cleanup

* remove redundent check

* bump version

* remove un-needed dependency

* cleanup

* cleanup file storage

* minor bug fix for buffer

* bug fix, old implementation needs to also pass in custom createdAt

* fix race condition

* review changes

* change naming and formatting

* add check to json string

* add calls to app callback

* remove dead line

* rename methods

* increase presist freq

* add locks

* add lock

* fix lock

* bump version back to beta
  • Loading branch information
KennyHuRadar authored Jan 4, 2024
1 parent a1c76fd commit dd0cdc0
Show file tree
Hide file tree
Showing 13 changed files with 464 additions and 69 deletions.
2 changes: 1 addition & 1 deletion sdk/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ apply plugin: "org.jetbrains.dokka"
apply plugin: 'io.radar.mvnpublish'

ext {
radarVersion = '3.8.18'
radarVersion = '3.8.18-beta.2'
}

String buildNumber = ".${System.currentTimeMillis()}"
Expand Down
39 changes: 35 additions & 4 deletions sdk/src/main/java/io/radar/sdk/Radar.kt
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@ object Radar {
}

if (!this::logBuffer.isInitialized) {
this.logBuffer = RadarSimpleLogBuffer()
this.logBuffer = RadarSimpleLogBuffer(this.context)
}

if (!this::replayBuffer.isInitialized) {
Expand Down Expand Up @@ -547,6 +547,10 @@ object Radar {
}
})

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
this.logger.logPastTermination()
}

this.initialized = true

logger.i("📍️ Radar initialized")
Expand Down Expand Up @@ -3088,6 +3092,29 @@ object Radar {
RadarSettings.setLogLevel(context, level)
}

/**
Log application resigning active.
*/
@JvmStatic
fun logResigningActive() {
if (!initialized) {
return
}
this.logger.logResigningActive()
}

/**
Log application entering background and flush logs in memory buffer into persistent buffer.
*/
@JvmStatic
fun logBackgrounding() {
if (!initialized) {
return
}
this.logger.logBackgrounding()
this.logBuffer.persistLogs()
}

/**
* Flushes debug logs to the server.
*/
Expand All @@ -3097,7 +3124,7 @@ object Radar {
return
}

val flushable = logBuffer.getFlushableLogsStash()
val flushable = logBuffer.getFlushableLogs()
val logs = flushable.get()
if (logs.isNotEmpty()) {
apiClient.log(logs, object : RadarApiClient.RadarLogCallback {
Expand Down Expand Up @@ -3378,11 +3405,15 @@ object Radar {
logger.e("📍️ Radar error received | status = $status", RadarLogType.SDK_ERROR)
}

internal fun sendLog(level: RadarLogLevel, message: String, type: RadarLogType?) {
internal fun sendLog(level: RadarLogLevel, message: String, type: RadarLogType?, createdAt: Date = Date()) {
receiver?.onLog(context, message)
if (isTestKey()) {
logBuffer.write(level, message, type)
logBuffer.write(level, type, message, createdAt)
}
}

internal fun setLogPersistenceFeatureFlag(enabled: Boolean) {
this.logBuffer.setPersistentLogFeatureFlag(enabled)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import android.view.InputDevice
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import io.radar.sdk.Radar.RadarStatus
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import io.radar.sdk.model.RadarConfig
Expand Down Expand Up @@ -93,6 +92,7 @@ internal class RadarActivityLifecycleCallbacks(
foreground = count > 0

updatePermissionsDenied(activity)
Radar.logResigningActive()
}

override fun onActivityStarted(activity: Activity) {
Expand All @@ -101,6 +101,7 @@ internal class RadarActivityLifecycleCallbacks(

override fun onActivityStopped(activity: Activity) {
updatePermissionsDenied(activity)
Radar.logBackgrounding()
}

override fun onActivityDestroyed(activity: Activity) {
Expand Down
55 changes: 55 additions & 0 deletions sdk/src/main/java/io/radar/sdk/RadarLogger.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
package io.radar.sdk

import android.app.ActivityManager
import java.text.SimpleDateFormat
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import io.radar.sdk.Radar.RadarLogLevel
import io.radar.sdk.Radar.RadarLogType
import java.util.Date
import java.util.Locale

internal class RadarLogger(
private val context: Context
Expand Down Expand Up @@ -49,4 +58,50 @@ internal class RadarLogger(
}
}

@RequiresApi(Build.VERSION_CODES.R)
fun logPastTermination(){
val activityManager = this.context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
val sharedPreferences = this.context.getSharedPreferences("RadarSDK", Context.MODE_PRIVATE)
val previousTimestamp = sharedPreferences.getLong("last_timestamp", 0)
val currentTimestamp = System.currentTimeMillis()
with(sharedPreferences.edit()) {
putLong("last_timestamp", currentTimestamp)
apply()
}
val batteryLevel = this.getBatteryLevel()

val crashLists = activityManager.getHistoricalProcessExitReasons(null, 0, 10)
if (crashLists.isNotEmpty()) {
for (crashInfo in crashLists) {
if (crashInfo.timestamp > previousTimestamp) {
Radar.sendLog(RadarLogLevel.INFO, "App terminating | with reason: ${crashInfo.getDescription()} | at ${dateFormat.format(Date(crashInfo.timestamp))} | with ${batteryLevel * 100}% battery", null, Date(crashInfo.timestamp))
break
}
}
}
}

fun getBatteryLevel(): Float {
val batteryStatus: Intent? = IntentFilter(Intent.ACTION_BATTERY_CHANGED).let { ifilter ->
this.context.registerReceiver(null, ifilter)
}
val level: Int = batteryStatus?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1
val scale: Int = batteryStatus?.getIntExtra(BatteryManager.EXTRA_SCALE, -1) ?: -1
val batteryPct: Float = level / scale.toFloat()
return batteryPct
}

fun logBackgrounding() {
val batteryLevel = this.getBatteryLevel()
val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
this.i("App entering background | at ${dateFormat.format(Date())} | with ${batteryLevel * 100}% battery")
}

fun logResigningActive() {
val batteryLevel = this.getBatteryLevel()
val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
this.i("App resigning active | at ${dateFormat.format(Date())} | with ${batteryLevel * 100}% battery")
}

}
9 changes: 8 additions & 1 deletion sdk/src/main/java/io/radar/sdk/RadarSettings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -294,14 +294,21 @@ internal object RadarSettings {
}

fun setFeatureSettings(context: Context, featureSettings: RadarFeatureSettings) {
Radar.setLogPersistenceFeatureFlag(featureSettings.useLogPersistence)
val optionsJson = featureSettings.toJson().toString()

getSharedPreferences(context).edit { putString(KEY_FEATURE_SETTINGS, optionsJson) }
}

fun getFeatureSettings(context: Context): RadarFeatureSettings {
val sharedPrefFeatureSettings = getSharedPreferences(context).getString(KEY_FEATURE_SETTINGS, null)
Radar.logger.d("getFeatureSettings | featureSettings = $sharedPrefFeatureSettings")
// The log buffer singleton is initialized before the logger, but requires calling this method
// to obtain its feature flag. Thus we cannot call the logger yet as its not yet initialized.
try {
Radar.logger.d("getFeatureSettings | featureSettings = $sharedPrefFeatureSettings")
} catch (e: Exception) {
// Do nothing for now
}
val optionsJson = sharedPrefFeatureSettings ?: return RadarFeatureSettings.default()
return RadarFeatureSettings.fromJson(JSONObject(optionsJson))
}
Expand Down
11 changes: 8 additions & 3 deletions sdk/src/main/java/io/radar/sdk/model/RadarFeatureSettings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@ internal data class RadarFeatureSettings(
val maxConcurrentJobs: Int,
val schedulerRequiresNetwork: Boolean,
val usePersistence: Boolean,
val extendFlushReplays: Boolean
val extendFlushReplays: Boolean,
val useLogPersistence: Boolean
) {
companion object {
private const val MAX_CONCURRENT_JOBS = "maxConcurrentJobs"
private const val DEFAULT_MAX_CONCURRENT_JOBS = 1
private const val USE_PERSISTENCE = "usePersistence"
private const val SCHEDULER_REQUIRES_NETWORK = "networkAny"
private const val EXTEND_FLUSH_REPLAYS = "extendFlushReplays"
private const val USE_LOG_PERSISTENCE = "useLogPersistence"

fun fromJson(json: JSONObject?): RadarFeatureSettings {
return if (json == null) {
Expand All @@ -26,7 +28,8 @@ internal data class RadarFeatureSettings(
json.optInt(MAX_CONCURRENT_JOBS, DEFAULT_MAX_CONCURRENT_JOBS),
json.optBoolean(SCHEDULER_REQUIRES_NETWORK),
json.optBoolean(USE_PERSISTENCE),
json.optBoolean(EXTEND_FLUSH_REPLAYS)
json.optBoolean(EXTEND_FLUSH_REPLAYS),
json.optBoolean(USE_LOG_PERSISTENCE)
)
}
}
Expand All @@ -36,7 +39,8 @@ internal data class RadarFeatureSettings(
DEFAULT_MAX_CONCURRENT_JOBS,
false, // networkAny
false, // usePersistence
false // extendFlushReplays
false, // extendFlushReplays
false // useLogPersistence
)
}
}
Expand All @@ -47,6 +51,7 @@ internal data class RadarFeatureSettings(
putOpt(SCHEDULER_REQUIRES_NETWORK, schedulerRequiresNetwork)
putOpt(USE_PERSISTENCE, usePersistence)
putOpt(EXTEND_FLUSH_REPLAYS, extendFlushReplays)
putOpt(USE_LOG_PERSISTENCE, useLogPersistence)
}
}
}
6 changes: 4 additions & 2 deletions sdk/src/main/java/io/radar/sdk/model/RadarLog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ internal data class RadarLog(

@JvmStatic
fun fromJson(json: JSONObject): RadarLog {
val levelString = json.optString(LEVEL)
val typeString = json.optString(TYPE)
return RadarLog(
level = Radar.RadarLogLevel.valueOf(json.optString(LEVEL)),
type = Radar.RadarLogType.valueOf(json.optString(TYPE)),
level = if (levelString.isNotBlank()) Radar.RadarLogLevel.valueOf(levelString) else Radar.RadarLogLevel.INFO,
type = if (typeString.isNotBlank() && typeString!="NONE") Radar.RadarLogType.valueOf(typeString) else null,
message = json.optString(MESSAGE),
createdAt = Date(json.optLong(CREATED_AT))
)
Expand Down
62 changes: 62 additions & 0 deletions sdk/src/main/java/io/radar/sdk/util/RadarFileStorage.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package io.radar.sdk.util
import android.content.Context
import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.FileOutputStream

class RadarFileStorage(private val context: Context) {
fun writeData(subDir: String = "", filename: String, content: String) {
var fileOutputStream: FileOutputStream
try {
val directory = if (subDir.isNotEmpty()) File(context.filesDir, subDir) else context.filesDir
directory.mkdirs()
val file = File(directory, filename)
fileOutputStream = FileOutputStream(file)
fileOutputStream.use { it.write(content.toByteArray()) }
} catch (e: Exception) {
e.printStackTrace()
}
}

fun readFileAtPath(subDir: String = "", filePath: String): String {
var fileInputStream: FileInputStream? = null
try {
val directory = if (subDir.isNotEmpty()) File(context.filesDir, subDir) else context.filesDir
val file = File(directory, filePath)
fileInputStream = FileInputStream(file)
val inputStreamReader = fileInputStream?.reader()
return inputStreamReader?.readText() ?: ""
} catch (e: FileNotFoundException) {
return ""
} catch (e: Exception) {
e.printStackTrace()
} finally {
fileInputStream?.close()
}
return ""
}

fun deleteFileAtPath(subDir: String = "", filePath: String): Boolean {
try {
val directory = if (subDir.isNotEmpty()) File(context.filesDir, subDir) else context.filesDir
val file = File(directory, filePath)
return file.delete()
} catch (e: Exception) {
e.printStackTrace()
}
return false
}

fun sortedFilesInDirectory(directoryPath: String, comparator: Comparator<File>): Array<File>? {
try {
val directory = File(context.filesDir, directoryPath)
var files = directory.listFiles()
files?.sortWith(comparator)
return files
} catch (e: Exception) {
e.printStackTrace()
}
return null
}
}
20 changes: 18 additions & 2 deletions sdk/src/main/java/io/radar/sdk/util/RadarLogBuffer.kt
Original file line number Diff line number Diff line change
@@ -1,23 +1,39 @@
package io.radar.sdk.util

import android.content.Context
import io.radar.sdk.Radar
import io.radar.sdk.model.RadarLog
import java.util.Date

internal interface RadarLogBuffer {

abstract val context: Context

/**
* Write a log to the buffer
*
* @param[level] log level
* @param[message] log message
*/
fun write(level: Radar.RadarLogLevel, message: String, type: Radar.RadarLogType?)
fun write(
level: Radar.RadarLogLevel,
type: Radar.RadarLogType?,
message: String,
createdAt: Date = Date()
)

/**
* Creates a stash of the logs currently in the buffer and returns them as a [Flushable] so that a successful
* callback can cleanup this log buffer by deleting old log files.
*
* @return a [Flushable] containing all stored logs
*/
fun getFlushableLogsStash(): Flushable<RadarLog>
fun getFlushableLogs(): Flushable<RadarLog>

/**
* Persist the logs to disk
*/
fun persistLogs()

fun setPersistentLogFeatureFlag(persistentLogFeatureFlag: Boolean)
}
Loading

0 comments on commit dd0cdc0

Please sign in to comment.