Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add content-based log filtering #83

Merged
merged 3 commits into from
Sep 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ Logging can be initialized via [`install`]:
Log.dispatcher.install(ConsoleLogger)
```

If no [`Logger`] is installed, then log blocks are not called at runtime.

Custom loggers can be created by implementing the [`Logger`] interface.

#### Android (Logcat)
Expand Down Expand Up @@ -134,6 +136,46 @@ enum class Sample {
}
```

### Filtering

Tuulbox implements log filtering by decorating [`Logger`]s.

#### Log Level Filters

Log level filters are installed with [`Logger.withMinimumLogLevel`]. Because the filtering is based on which log call is
made, instead of the content of the log call, these can be used as an optimization: if all [`Logger`]s installed in the
root [`DispatchLogger`] have a minimum log level higher than the log call being made, then the log block is never called.

```kotlin
Log.dispatcher.install(
ConsoleLogger
.withMinimumLogLevel(LogLevel.Warn)
)

Log.debug { "This is not called." }
Log.warn { "This still gets called." }
```

#### Log Content Filters

Log content filters are installed with [`Logger.withFilter`], and have full access to the content of a log.


```kotlin
Log.dispatcher.install(
ConsoleLogger
.withFilter { tag, message, metadata, throwable ->
metadata[Sensitivity] == Sensitivity.NotSensitive
}
)

Log.debug { "This block is evaluated, but does not get printed to the console." }
Log.warn { metadata ->
metadata[Sensitivity] = Sensitivity.NotSensitive
"This is also evaluated, and does print to the console."
}
```

## [Functional](https://juullabs.github.io/tuulbox/functional/index.html)

![badge-ios]
Expand Down Expand Up @@ -361,6 +403,9 @@ limitations under the License.
[`WriteMetadata`]: https://juullabs.github.io/tuulbox/logging/logging/com.juul.tuulbox.logging/-write-metadata/index.html
[`ReadMetadata`]: https://juullabs.github.io/tuulbox/logging/logging/com.juul.tuulbox.logging/-read-metadata/index.html
[`Key`]: https://juullabs.github.io/tuulbox/logging/logging/com.juul.tuulbox.logging/-key/index.html
[`Logger.withMinimumLogLevel`]: https://juullabs.github.io/tuulbox/logging/logging/com.juul.tuulbox.logging/with-minimum-log-level.html
[`DispatchLogger`]: https://juullabs.github.io/tuulbox/logging/logging/com.juul.tuulbox.logging/-dispatch-logger/index.html
[`Logger.withFilter`]: https://juullabs.github.io/tuulbox/logging/logging/com.juul.tuulbox.logging/with-filter.html
[`runTest`]: https://juullabs.github.io/tuulbox/test/test/com.juul.tuulbox.test/run-test.html
[`assertContains`]: https://juullabs.github.io/tuulbox/test/test/com.juul.tuulbox.test/assert-contains.html
[`assertSimilar`]: https://juullabs.github.io/tuulbox/test/test/com.juul.tuulbox.test/assert-similar.html
Expand Down
54 changes: 54 additions & 0 deletions logging/src/commonMain/kotlin/FilterLogger.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.juul.tuulbox.logging

public fun interface LogFilter {
public fun canLog(tag: String, message: String, metadata: ReadMetadata, throwable: Throwable?): Boolean
}

public fun Logger.withFilter(
filter: LogFilter
): Logger = FilterLogger(filter, this)

private class FilterLogger(
private val filter: LogFilter,
private val inner: Logger,
) : Logger {

override val minimumLogLevel: LogLevel
get() = inner.minimumLogLevel

override fun verbose(tag: String, message: String, metadata: ReadMetadata, throwable: Throwable?) {
if (filter.canLog(tag, message, metadata, throwable)) {
inner.verbose(tag, message, metadata, throwable)
}
}

override fun debug(tag: String, message: String, metadata: ReadMetadata, throwable: Throwable?) {
if (filter.canLog(tag, message, metadata, throwable)) {
inner.debug(tag, message, metadata, throwable)
}
}

override fun info(tag: String, message: String, metadata: ReadMetadata, throwable: Throwable?) {
if (filter.canLog(tag, message, metadata, throwable)) {
inner.info(tag, message, metadata, throwable)
}
}

override fun warn(tag: String, message: String, metadata: ReadMetadata, throwable: Throwable?) {
if (filter.canLog(tag, message, metadata, throwable)) {
inner.warn(tag, message, metadata, throwable)
}
}

override fun error(tag: String, message: String, metadata: ReadMetadata, throwable: Throwable?) {
if (filter.canLog(tag, message, metadata, throwable)) {
inner.error(tag, message, metadata, throwable)
}
}

override fun assert(tag: String, message: String, metadata: ReadMetadata, throwable: Throwable?) {
if (filter.canLog(tag, message, metadata, throwable)) {
inner.assert(tag, message, metadata, throwable)
}
}
}
141 changes: 141 additions & 0 deletions logging/src/commonTest/kotlin/FilterLoggerTests.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package com.juul.tuulbox.logging

import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertSame
import kotlin.test.assertTrue

class FilterLoggerTests {

@Test
fun verbose_whenDenied_doesNotCallInner() {
val inner = CallListLogger()
val throwable = Throwable()
inner.withFilter { _, _, _, _ -> false }
.verbose("tag", "message", Metadata(), throwable)
assertTrue(inner.verboseCalls.isEmpty())
}

@Test
fun verbose_whenPermitted_callsInnerWithSameArguments() {
val inner = CallListLogger()
val throwable = Throwable()
inner.withFilter { _, _, _, _ -> true }
.verbose("tag", "message", Metadata(), throwable)
val call = inner.verboseCalls.single()
assertEquals("tag", call.tag)
assertEquals("message", call.message)
assertEquals(Metadata(), call.metadata)
assertSame(throwable, call.throwable)
}

@Test
fun debug_whenDenied_doesNotCallInner() {
val inner = CallListLogger()
val throwable = Throwable()
inner.withFilter { _, _, _, _ -> false }
.debug("tag", "message", Metadata(), throwable)
assertTrue(inner.debugCalls.isEmpty())
}

@Test
fun debug_whenPermitted_callsInnerWithSameArguments() {
val inner = CallListLogger()
val throwable = Throwable()
inner.withFilter { _, _, _, _ -> true }
.debug("tag", "message", Metadata(), throwable)
val call = inner.debugCalls.single()
assertEquals("tag", call.tag)
assertEquals("message", call.message)
assertEquals(Metadata(), call.metadata)
assertSame(throwable, call.throwable)
}

@Test
fun info_whenDenied_doesNotCallInner() {
val inner = CallListLogger()
val throwable = Throwable()
inner.withFilter { _, _, _, _ -> false }
.info("tag", "message", Metadata(), throwable)
assertTrue(inner.infoCalls.isEmpty())
}

@Test
fun info_whenPermitted_callsInnerWithSameArguments() {
val inner = CallListLogger()
val throwable = Throwable()
inner.withFilter { _, _, _, _ -> true }
.info("tag", "message", Metadata(), throwable)
val call = inner.infoCalls.single()
assertEquals("tag", call.tag)
assertEquals("message", call.message)
assertEquals(Metadata(), call.metadata)
assertSame(throwable, call.throwable)
}

@Test
fun warn_whenDenied_doesNotCallInner() {
val inner = CallListLogger()
val throwable = Throwable()
inner.withFilter { _, _, _, _ -> false }
.warn("tag", "message", Metadata(), throwable)
assertTrue(inner.warnCalls.isEmpty())
}

@Test
fun warn_whenPermitted_callsInnerWithSameArguments() {
val inner = CallListLogger()
val throwable = Throwable()
inner.withFilter { _, _, _, _ -> true }
.warn("tag", "message", Metadata(), throwable)
val call = inner.warnCalls.single()
assertEquals("tag", call.tag)
assertEquals("message", call.message)
assertEquals(Metadata(), call.metadata)
assertSame(throwable, call.throwable)
}

@Test
fun error_whenDenied_doesNotCallInner() {
val inner = CallListLogger()
val throwable = Throwable()
inner.withFilter { _, _, _, _ -> false }
.error("tag", "message", Metadata(), throwable)
assertTrue(inner.errorCalls.isEmpty())
}

@Test
fun error_whenPermitted_callsInnerWithSameArguments() {
val inner = CallListLogger()
val throwable = Throwable()
inner.withFilter { _, _, _, _ -> true }
.error("tag", "message", Metadata(), throwable)
val call = inner.errorCalls.single()
assertEquals("tag", call.tag)
assertEquals("message", call.message)
assertEquals(Metadata(), call.metadata)
assertSame(throwable, call.throwable)
}

@Test
fun assert_whenDenied_doesNotCallInner() {
val inner = CallListLogger()
val throwable = Throwable()
inner.withFilter { _, _, _, _ -> false }
.assert("tag", "message", Metadata(), throwable)
assertTrue(inner.assertCalls.isEmpty())
}

@Test
fun assert_whenPermitted_callsInnerWithSameArguments() {
val inner = CallListLogger()
val throwable = Throwable()
inner.withFilter { _, _, _, _ -> true }
.assert("tag", "message", Metadata(), throwable)
val call = inner.assertCalls.single()
assertEquals("tag", call.tag)
assertEquals("message", call.message)
assertEquals(Metadata(), call.metadata)
assertSame(throwable, call.throwable)
}
}