From f0616cfcfecd59bc1194c3ce9617c5b86dcdbad3 Mon Sep 17 00:00:00 2001 From: Paul Hawke Date: Tue, 30 Apr 2024 22:53:47 -0500 Subject: [PATCH] Chunked log writer that breaks long messages before passing the log on to a wrapped log writer instance --- .../co/touchlab/kermit/ChunkedLogWriter.kt | 54 ++++++++++++ .../touchlab/kermit/ChunkedLogWriterTest.kt | 82 +++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 kermit-core/src/commonMain/kotlin/co/touchlab/kermit/ChunkedLogWriter.kt create mode 100644 kermit-core/src/commonTest/kotlin/co/touchlab/kermit/ChunkedLogWriterTest.kt diff --git a/kermit-core/src/commonMain/kotlin/co/touchlab/kermit/ChunkedLogWriter.kt b/kermit-core/src/commonMain/kotlin/co/touchlab/kermit/ChunkedLogWriter.kt new file mode 100644 index 00000000..f332680c --- /dev/null +++ b/kermit-core/src/commonMain/kotlin/co/touchlab/kermit/ChunkedLogWriter.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2024 Touchlab + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package co.touchlab.kermit + +class ChunkedLogWriter( + internal val wrapped: LogWriter, + private val maxMessageLength: Int, + private val minMessageLength: Int +) : LogWriter() { + override fun log(severity: Severity, message: String, tag: String, throwable: Throwable?) { + chunkedLog(severity, message, tag, throwable) + } + + private tailrec fun chunkedLog( + severity: Severity, + message: String, + tag: String, + throwable: Throwable? + ) { + if (message.length > maxMessageLength) { + var msgSubstring = message.substring(0, maxMessageLength) + var msgSubstringEndIndex = maxMessageLength + + // Try to find a substring break at a newline char. + msgSubstring.lastIndexOf('\n').let { lastIndex -> + if (lastIndex >= minMessageLength) { + msgSubstring = msgSubstring.substring(0, lastIndex) + // skip over new line char + msgSubstringEndIndex = lastIndex + 1 + } + } + + // Log the substring. + wrapped.log(severity, msgSubstring, tag, throwable) + + // Recursively log the remainder. + chunkedLog(severity, message.substring(msgSubstringEndIndex), tag, throwable) + } else { + wrapped.log(severity, message, tag, throwable) + } + } +} + +fun LogWriter.chunked(maxMessageLength: Int = 4000, minMessageLength: Int = 3000): LogWriter = + ChunkedLogWriter(this, maxMessageLength, minMessageLength) + diff --git a/kermit-core/src/commonTest/kotlin/co/touchlab/kermit/ChunkedLogWriterTest.kt b/kermit-core/src/commonTest/kotlin/co/touchlab/kermit/ChunkedLogWriterTest.kt new file mode 100644 index 00000000..c5c711a9 --- /dev/null +++ b/kermit-core/src/commonTest/kotlin/co/touchlab/kermit/ChunkedLogWriterTest.kt @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2024 Touchlab + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package co.touchlab.kermit + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertSame + +@OptIn(ExperimentalKermitApi::class) +class ChunkedLogWriterTest { + private val testLogWriter = TestLogWriter(Severity.Warn) + private val testObject = testLogWriter.chunked(12, 8) as ChunkedLogWriter + + @Test + fun returnsWrappedWriter() { + assertSame(testLogWriter, testObject.wrapped) + } + + @Test + fun nonChunkedOutput() { + testObject.log(Severity.Error, "test", "my-tag") + + assertEquals(1, testLogWriter.logs.size) + + with(testLogWriter.logs[0]) { + assertEquals(Severity.Error, severity) + assertEquals("my-tag", tag) + assertEquals("test", message) + assertEquals(null, throwable) + } + } + + @Test + fun twoChunksNoLinebreak() { + testObject.log(Severity.Error, "test test test test", "my-tag") + + assertEquals(2, testLogWriter.logs.size) + + with(testLogWriter.logs[0]) { + assertEquals(Severity.Error, severity) + assertEquals("my-tag", tag) + assertEquals("test test te", message) + assertEquals(null, throwable) + } + + with(testLogWriter.logs[1]) { + assertEquals(Severity.Error, severity) + assertEquals("my-tag", tag) + assertEquals("st test", message) + assertEquals(null, throwable) + } + } + + @Test + fun twoChunksWithLinebreak() { + testObject.log(Severity.Error, "test test\ntest test", "my-tag") + + assertEquals(2, testLogWriter.logs.size) + + with(testLogWriter.logs[0]) { + assertEquals(Severity.Error, severity) + assertEquals("my-tag", tag) + assertEquals("test test", message) + assertEquals(null, throwable) + } + + with(testLogWriter.logs[1]) { + assertEquals(Severity.Error, severity) + assertEquals("my-tag", tag) + assertEquals("test test", message) + assertEquals(null, throwable) + } + } +} \ No newline at end of file