logging: address review of the SQL logging path

Only downgrade a uniqueness conflict on an INSERT, so genuine duplicate-key failures still surface. Clear old zips from the dump directory before exporting, roll the log file over at midnight, prune on roll/day-change instead of on every line, and drop the unused files-path from the FileProvider config.
This commit is contained in:
austinried
2026-10-09 07:58:55 +00:00
parent 713d9a976a
commit 9d783b2301
4 changed files with 43 additions and 17 deletions
+12 -10
View File
@@ -108,7 +108,7 @@ object Log {
fun sql(execution: SqlExecution) { fun sql(execution: SqlExecution) {
val failure = execution.error val failure = execution.error
if (failure != null) { if (failure != null) {
if (isUpsertConflict(failure)) { if (isUpsertConflict(execution.sql, failure)) {
d("sql", "upsert fallback conflict: ${shortSql(execution.sql)}") d("sql", "upsert fallback conflict: ${shortSql(execution.sql)}")
} else { } else {
e("sql", "failed: ${shortSql(execution.sql)}", failure) e("sql", "failed: ${shortSql(execution.sql)}", failure)
@@ -180,16 +180,18 @@ internal fun isSlowQuery(
thresholdMs: Double, thresholdMs: Double,
): Boolean = execution.error == null && execution.durationMillis >= thresholdMs && isDataStatement(execution.sql) ): Boolean = execution.error == null && execution.durationMillis >= thresholdMs && isDataStatement(execution.sql)
// Room's EntityUpsertAdapter implements @Upsert as an INSERT and, on a uniqueness conflict, catches // Room's EntityUpsertAdapter implements @Upsert as an INSERT and treats a uniqueness conflict as the
// the error and runs an UPDATE instead (EntityUpsertAdapter.checkUniquenessException). That failure // signal to UPDATE instead (EntityUpsertAdapter.checkUniquenessException). Only downgrade a failed
// is expected control flow, not an error worth alarming about. // INSERT whose error says the conflict was a unique/primary key; anything else stays an ERROR so a
// genuine failure isn't hidden in release.
private const val CONSTRAINT_UNIQUE_MESSAGE = "unique constraint failed" private const val CONSTRAINT_UNIQUE_MESSAGE = "unique constraint failed"
private const val SQLITE_CONSTRAINT_PRIMARYKEY = "1555" private const val CONSTRAINT_PRIMARY_KEY_MESSAGE = "primary key constraint failed"
private const val SQLITE_CONSTRAINT_UNIQUE = "2067"
internal fun isUpsertConflict(error: Throwable): Boolean { internal fun isUpsertConflict(
sql: String,
error: Throwable,
): Boolean {
if (!sql.trimStart().uppercase().startsWith("INSERT")) return false
val message = error.message?.lowercase() ?: return false val message = error.message?.lowercase() ?: return false
return message.contains(CONSTRAINT_UNIQUE_MESSAGE) || return message.contains(CONSTRAINT_UNIQUE_MESSAGE) || message.contains(CONSTRAINT_PRIMARY_KEY_MESSAGE)
message.contains(SQLITE_CONSTRAINT_PRIMARYKEY) ||
message.contains(SQLITE_CONSTRAINT_UNIQUE)
} }
@@ -25,6 +25,10 @@ class LogFileStore(
private val lineFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.US) private val lineFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.US)
private var current = currentFile() private var current = currentFile()
init {
prune()
}
fun write( fun write(
level: LogLevel, level: LogLevel,
tag: String, tag: String,
@@ -66,6 +70,7 @@ class LogFileStore(
val logs = files() val logs = files()
if (logs.isEmpty()) return null if (logs.isEmpty()) return null
destDir.mkdirs() destDir.mkdirs()
destDir.listFiles()?.forEach { if (it.isFile) it.delete() }
val zip = File(destDir, "subtracks-logs-${System.currentTimeMillis()}.zip") val zip = File(destDir, "subtracks-logs-${System.currentTimeMillis()}.zip")
ZipOutputStream(zip.outputStream().buffered()).use { out -> ZipOutputStream(zip.outputStream().buffered()).use { out ->
logs.forEach { log -> logs.forEach { log ->
@@ -100,9 +105,18 @@ class LogFileStore(
append('\n') append('\n')
} }
rootDir.mkdirs() rootDir.mkdirs()
if (current.length() >= maxFileBytes) roll() var rolled = false
val today = currentFile()
if (current.name != today.name) {
current = today
rolled = true
}
if (current.length() >= maxFileBytes) {
roll()
rolled = true
}
current.appendText(redactLogs(text)) current.appendText(redactLogs(text))
prune() if (rolled) prune()
} }
private var rollSequence = 0 private var rollSequence = 0
-1
View File
@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<paths> <paths>
<files-path name="logs" path="logs/" />
<cache-path name="log_dumps" path="log_dumps/" /> <cache-path name="log_dumps" path="log_dumps/" />
</paths> </paths>
@@ -152,10 +152,21 @@ class LogFileStoreTest {
@Test @Test
fun detectsUpsertConflictErrors() { fun detectsUpsertConflictErrors() {
assertTrue(isUpsertConflict(IllegalStateException("UNIQUE constraint failed: playback_cursor.id"))) assertTrue(
assertTrue(isUpsertConflict(IllegalStateException("Error code: 1555, message: UNIQUE constraint failed"))) isUpsertConflict(
assertTrue(isUpsertConflict(IllegalStateException("SQLITE_CONSTRAINT_UNIQUE 2067"))) "INSERT INTO playback_cursor (id) VALUES (?)",
assertFalse(isUpsertConflict(IllegalStateException("no such table: songs"))) IllegalStateException("UNIQUE constraint failed: playback_cursor.id"),
),
)
assertTrue(
isUpsertConflict(
"INSERT OR ABORT INTO t VALUES (?)",
IllegalStateException("PRIMARY KEY constraint failed"),
),
)
assertFalse(isUpsertConflict("UPDATE t SET x = 1", IllegalStateException("UNIQUE constraint failed: t.x")))
assertFalse(isUpsertConflict("SELECT * FROM t", IllegalStateException("no such table: t")))
assertFalse(isUpsertConflict("INSERT INTO t VALUES (?)", IllegalStateException("Error code: 1555")))
} }
private fun execution( private fun execution(