Skip to content
This repository has been archived by the owner on Jun 20, 2023. It is now read-only.

Fix debug log censoring collisions (EXPOSUREAPP-7196) #3230

Merged
merged 20 commits into from
May 21, 2021

Conversation

d4rken
Copy link
Member

@d4rken d4rken commented May 19, 2021

@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:

message = "Hello world

after module 1:
message = "Hello"

module 2 wants to replace "Hello world", but only finds "Hello" which is no match.

In this PR @kolyaopahle and me reworked the mechanism:

  • The censoring mechanism to longer run in serial on the data that should be censored. All censor modules run at the same time.
  • Each censor module now produces a CensoredString(String,IntRage). With String being the changed message, and IntRange the start and end index of what was modified in the original log message.
  • After all censors are done, the 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.

@d4rken d4rken added the maintainers Tag pull requests created by maintainers label May 19, 2021
@d4rken d4rken added this to the 2.3.0 milestone May 19, 2021
@d4rken d4rken requested a review from a team May 19, 2021 16:43
Comment on lines +19 to +29
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)
}
Copy link
Member Author

@d4rken d4rken May 20, 2021

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

Comment on lines 31 to 40
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
)
}
Copy link
Member Author

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()
Copy link
Member Author

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.

Comment on lines +168 to +176
val censored: Collection<BugCensor.CensoredString> = bugCensors.get()
.map {
async {
it.checkLog(formattedMessage)
}
}
.awaitAll()
.filterNotNull()
.filter { it.range != null }
Copy link
Member Author

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.

Comment on lines +182 to +184
val minMin = censored.minOf { it.range!!.first }
val maxMax = censored.maxOf { it.range!!.last }
formattedMessage.replaceRange(minMin, maxMax, CENSOR_COLLISION_PLACERHOLDER)
Copy link
Member Author

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.

Comment on lines +21 to +25
return if (throwable != null) {
baseLine + "\n" + getStackTraceString(throwable)
} else {
baseLine
}
Copy link
Member Author

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 😦

@d4rken d4rken added bug Something isn't working author merge PR to be merged by author labels May 20, 2021
@d4rken
Copy link
Member Author

d4rken commented May 20, 2021

Merge this after #3230

@LukasLechnerDev LukasLechnerDev self-assigned this May 20, 2021
@harambasicluka
Copy link
Contributor

harambasicluka commented May 20, 2021

Merge this after #3230

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,
Copy link
Contributor

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?

Copy link
Member Author

@d4rken d4rken May 20, 2021

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.

Copy link
Contributor

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 👍

Copy link
Contributor

@LukasLechnerDev LukasLechnerDev left a 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.

@d4rken d4rken removed the author merge PR to be merged by author label May 20, 2021
@d4rken
Copy link
Member Author

d4rken commented May 20, 2021

Merge this after #3230

Deadlock!

😄 I meant #3235

Copy link
Contributor

@LukasLechnerDev LukasLechnerDev left a 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 👍

@mtwalli mtwalli self-assigned this May 21, 2021
@sonarcloud
Copy link

sonarcloud bot commented May 21, 2021

Kudos, SonarCloud Quality Gate passed!

Bug A 0 Bugs
Vulnerability A 0 Vulnerabilities
Security Hotspot A 0 Security Hotspots
Code Smell A 0 Code Smells

87.0% 87.0% Coverage
0.0% 0.0% Duplication

@harambasicluka harambasicluka merged commit 4e47438 into release/2.3.x May 21, 2021
@harambasicluka harambasicluka deleted the fix/7196-debug-censoring-collisions branch May 21, 2021 10:40
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
bug Something isn't working maintainers Tag pull requests created by maintainers
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants