-
Notifications
You must be signed in to change notification settings - Fork 499
Fix debug log censoring collisions (EXPOSUREAPP-7196) #3230
Fix debug log censoring collisions (EXPOSUREAPP-7196) #3230
Conversation
Give the debug logger an additional thread to parallelize censoring.
operator fun CensoredString.plus(newer: CensoredString?): CensoredString { | ||
if (newer == null) return this | ||
|
||
val range = when { | ||
newer.range == null -> this.range | ||
this.range == null -> newer.range | ||
else -> min(this.range.first, newer.range.first)..max(this.range.last, newer.range.last) | ||
} | ||
|
||
return CensoredString(string = newer.string, range = range) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Operator to make chaining censoring easier by being able to use var old += new
fun CensoredString.censor(orig: String, replacement: String): CensoredString? { | ||
val start = this.string.indexOf(orig) | ||
if (start == -1) return null | ||
|
||
val end = start + replacement.length | ||
return CensoredString( | ||
string = this.string.replace(orig, replacement), | ||
range = start..end | ||
) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Acts like String.replace
but tells us the start+end index of the match.
val censoredLine = bugCensors.get().fold(rawLine) { prev, censor -> | ||
censor.checkLog(prev) ?: prev | ||
|
||
val formattedMessage = rawLine.format() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As we don't need to censor logtags, we pre compute the log message and only pass that to censors.
val censored: Collection<BugCensor.CensoredString> = bugCensors.get() | ||
.map { | ||
async { | ||
it.checkLog(formattedMessage) | ||
} | ||
} | ||
.awaitAll() | ||
.filterNotNull() | ||
.filter { it.range != null } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kolyaopahle and me performance tested the censoring and we were at ~100ms if we run all censors in sequence. This now runs in parallel the DebugScope
threads, reaching 10-20ms per logline.
For all bug censors that are stateful (i.e. do local caching), thread safe has to be taken into consideration though.
val minMin = censored.minOf { it.range!!.first } | ||
val maxMax = censored.maxOf { it.range!!.last } | ||
formattedMessage.replaceRange(minMin, maxMax, CENSOR_COLLISION_PLACERHOLDER) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Part that deletes whole sections if there was a censoring collision.
return if (throwable != null) { | ||
baseLine + "\n" + getStackTraceString(throwable) | ||
} else { | ||
baseLine | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We previously overlooked that the actual stacktrace was not written to log 😦
Merge this after #3230 |
Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/CWADebug.kt
Outdated
Show resolved
Hide resolved
# Conflicts: # Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/internal/LogSnapshotterTest.kt
Deadlock! |
@@ -9,7 +9,8 @@ import javax.inject.Qualifier | |||
import kotlin.coroutines.CoroutineContext | |||
|
|||
object DebugLoggerScope : CoroutineScope { | |||
val dispatcher = Executors.newSingleThreadExecutor( | |||
val dispatcher = Executors.newFixedThreadPool( | |||
4, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why exactly 4 here? Would it make sense to use Dispatchers.Default?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We tested 1 to 16 threads, 4 yielded the most performance gain with the least amount of threads.
- 1 thread ~ 110ms per logline on avg
- 2 ~ 50ms
- 3 ~ 35ms
- 4 ~ 20ms
- 5 ~ 15-20ms
- 6 ~ 11-20ms
etc. diminishing returns
Using Dispatchers.Default
would not be bad, but we decided against it to reduce impact on other app operations.
Dispatchers.Default
provides a thread count equal to the number of cores, e.g. 4, if the logging mechanisms block for some reasons, the coroutines running on Default
through out the app could starve as fewer or even no threads are available. I don't think this would happen, but just to be sure it can't and to reduce the impact of logging on the app and to make the behavior recorded by the logger more accurate to what is happening when it is not active, it is nicer to work on different threads here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes Sense! Thanks for the in-depth explanation 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. Tested it on device and got the <censoring-collision>
in the log.
…h, not it's replacement.
…ons' into fix/7196-debug-censoring-collisions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM#2. Thank you for the new unit test 👍
Kudos, SonarCloud Quality Gate passed! |
@kolyaopahle found an issue for some uncensored log entries. The underlying cause was that if there is a collision between different censors, i.e. that two censor modules replace elements on the same line, the original string does not match what the other censor module expects. Example:
In this PR @kolyaopahle and me reworked the mechanism:
CensoredString(String,IntRage)
. WithString
being the changed message, andIntRange
the start and end index of what was modified in the original log message.DebugLogger
checks if more than one censor module modified the original message. If that is the case, the minimum and maximum index of modifications in the original log message is removed and replaced with a placeholder<censoring-collision>
. This leaves us with some information that is useful for debugging and also removes any information that should have been censored.