mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 15:29:15 +01:00
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:
@@ -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,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(
|
||||||
|
|||||||
Reference in New Issue
Block a user