diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 714cf77..6125316 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,11 +1,9 @@ # Contributing ## Commit messages - Before writing a commit message read [this article](https://chris.beams.io/posts/git-commit/). ## Build - Before pushing any changes make sure project builds without errors with: ``` @@ -13,14 +11,12 @@ Before pushing any changes make sure project builds without errors with: ``` ## Unit tests - This project uses [Kotest](https://kotest.io/) for testing. - please use the `Spec` suffix for new test classes - make sure tests clearly document new feature ## Validate changes locally - Before submitting a pull request test your changes locally on a sample project. There are few ways for local testing: @@ -28,7 +24,22 @@ There are few ways for local testing: - or publish library to maven local repository with `./gradlew publishToMavenLocal` and use it in any project via [`mavenLocal()`](https://docs.gradle.org/current/userguide/declaring_repositories.html#sub:maven_local) repository -## Documentation +## Validating with snapshot release +Snapshot release is triggered automatically after merge to the main branch. +To use a released snapshot version make sure to register Sonatype snapshot repository in gradle with: +``` +// build.gradle.kts +repositories { + mavenCentral() + maven { + url = URI("https://oss.sonatype.org/content/repositories/snapshots") + } +} +``` + +The snapshot version can be found in GitHub Action build log. + +## Documentation If change adds new feature or modifies a new one update [documentation](https://github.com/coditory/klog/tree/master/samples). diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index 8bf692e..eb8dc6a 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -3,27 +3,27 @@ name: Bug report about: Reporting bugs and other issues labels: bug --- - - - ## Context - - + + - ## Expected Behavior - +## Context + + - ## Observed Behavior - +## Expected Behavior + - ## Steps to Reproduce - - +## Observed Behavior + - ## Your Environment - - * Library version: - * Java (and/or Kotlin) version: - * Gradle version: - * Gradle scan link (add `--scan` option when running the gradle task): - * Link to your project (if it's a public repository): +## Steps to Reproduce + + + +## Your Environment + + +* Library version: +* Java (and/or Kotlin) version: +* Link to your project (if it's a public repository): diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md index ccbefd2..ebc5025 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.md +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -3,7 +3,7 @@ name: Feature request about: Suggest new features/changes labels: feature --- - + ## Context @@ -11,8 +11,8 @@ labels: feature ## Expected Behavior - + ## Current Behavior - + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index c4c2c10..5233f84 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,15 +2,12 @@ ## Changes - - + + ## Checklist - [ ] I have tested that there is no similar [pull request](https://github.com/coditory/klog/pulls) already submitted -- [ ] I have read [contributing.md](https://github.com/coditory/klog/blob/master/.github/CONTRIBUTING.md) and applied to - the rules +- [ ] I have read [contributing.md](https://github.com/coditory/klog/blob/master/.github/CONTRIBUTING.md) and applied to the rules - [ ] I have unit tested code changes and performed a self-review -- [ ] I - have [tested plugin change locally](https://github.com/coditory/klog/blob/master/.github/CONTRIBUTING.md#validate-changes-locally) - on a sample project +- [ ] I have [tested plugin change locally](https://github.com/coditory/klog/blob/master/.github/CONTRIBUTING.md#validate-changes-locally) on a sample project diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 0000000..73d56db --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,14 @@ +name: Labeler + +on: [pull_request_target] + +jobs: + labeler: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - uses: actions/labeler@v4 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/klog-sample/src/main/kotlin/com/coditory/klog/sample/SampleRunner.kt b/klog-sample/src/main/kotlin/com/coditory/klog/sample/SampleRunner.kt index e2e1bac..e112f01 100644 --- a/klog-sample/src/main/kotlin/com/coditory/klog/sample/SampleRunner.kt +++ b/klog-sample/src/main/kotlin/com/coditory/klog/sample/SampleRunner.kt @@ -6,6 +6,8 @@ object SampleRunner { @JvmStatic fun main(args: Array) { val l = Klog.logger() + val e = IllegalArgumentException("Sample exception") + l.error(e) { "Testing error" } // runBlocking { // repeat(10000) { // launch { diff --git a/klog/src/main/kotlin/com/coditory/klog/Klog.kt b/klog/src/main/kotlin/com/coditory/klog/Klog.kt index 58223d8..048ad36 100644 --- a/klog/src/main/kotlin/com/coditory/klog/Klog.kt +++ b/klog/src/main/kotlin/com/coditory/klog/Klog.kt @@ -1,12 +1,19 @@ package com.coditory.klog import com.coditory.klog.config.KlogConfig +import com.coditory.klog.config.KlogConfigBuilder import com.coditory.klog.config.klogConfig import com.coditory.klog.publish.SystemOutPublisher import kotlinx.coroutines.runBlocking import java.util.concurrent.ConcurrentHashMap import kotlin.reflect.KClass +fun klog(init: KlogConfigBuilder.() -> Unit): Klog { + val config = KlogConfigBuilder() + config.init() + return Klog(config.build()) +} + class Klog(config: KlogConfig) { private val loggers = ConcurrentHashMap() private var context: KlogContext = KlogContext.build(config) @@ -71,6 +78,12 @@ class Klog(config: KlogConfig) { GLOBAL_INSTANCE.reconfigure(config) } + fun configure(init: KlogConfigBuilder.() -> Unit) { + val config = KlogConfigBuilder() + config.init() + GLOBAL_INSTANCE.reconfigure(config.build()) + } + fun flush() { GLOBAL_INSTANCE.flush() } diff --git a/klog/src/main/kotlin/com/coditory/klog/LogEventField.kt b/klog/src/main/kotlin/com/coditory/klog/LogEventField.kt index fe88f8d..f359731 100644 --- a/klog/src/main/kotlin/com/coditory/klog/LogEventField.kt +++ b/klog/src/main/kotlin/com/coditory/klog/LogEventField.kt @@ -8,6 +8,7 @@ enum class LogEventField { LOGGER, MESSAGE, ITEMS, + EXCEPTION, ; internal fun skip( @@ -27,7 +28,7 @@ enum class LogEventField { companion object { fun all(): List { - return listOf(TIMESTAMP, LEVEL, THREAD, CONTEXT, LOGGER, MESSAGE, ITEMS) + return listOf(TIMESTAMP, LEVEL, THREAD, CONTEXT, LOGGER, MESSAGE, ITEMS, EXCEPTION) } } } diff --git a/klog/src/main/kotlin/com/coditory/klog/publish/InMemoryPublisher.kt b/klog/src/main/kotlin/com/coditory/klog/publish/InMemoryPublisher.kt index a8ffe99..e833c5b 100644 --- a/klog/src/main/kotlin/com/coditory/klog/publish/InMemoryPublisher.kt +++ b/klog/src/main/kotlin/com/coditory/klog/publish/InMemoryPublisher.kt @@ -12,4 +12,8 @@ class InMemoryPublisher : BlockingPublisher { fun getLogs(): List = events fun getLastLog(): LogEvent? = events.lastOrNull() + + fun clear() { + events.clear() + } } diff --git a/klog/src/main/kotlin/com/coditory/klog/publish/SystemOutPublisher.kt b/klog/src/main/kotlin/com/coditory/klog/publish/SystemOutPublisher.kt index ccc2c50..c4010c9 100644 --- a/klog/src/main/kotlin/com/coditory/klog/publish/SystemOutPublisher.kt +++ b/klog/src/main/kotlin/com/coditory/klog/publish/SystemOutPublisher.kt @@ -49,7 +49,7 @@ class SystemOutPublisher( } fun json(): SystemOutPublisher { - val formatter = JsonLogEventSerializer.default() + val formatter = JsonLogEventSerializer() return SystemOutPublisher(formatter) } } diff --git a/klog/src/main/kotlin/com/coditory/klog/text/json/JsonEscapedAppendable.kt b/klog/src/main/kotlin/com/coditory/klog/text/json/JsonEscapedAppendable.kt index fa8174d..e3afe03 100644 --- a/klog/src/main/kotlin/com/coditory/klog/text/json/JsonEscapedAppendable.kt +++ b/klog/src/main/kotlin/com/coditory/klog/text/json/JsonEscapedAppendable.kt @@ -1,11 +1,22 @@ package com.coditory.klog.text.json +import com.coditory.klog.text.shared.SizedAppendable.Companion.MAX_LENGTH_MARKER +import kotlin.math.min + internal class JsonEscapedAppendable( private val appendable: Appendable, + private val maxLength: Int = Int.MAX_VALUE, + private val maxLengthMarker: String = MAX_LENGTH_MARKER, ) : Appendable { + private var length = 0 + override fun append(csq: CharSequence?): Appendable { + if (length >= maxLength) return this if (csq != null) { - JsonEncoder.encode(csq, appendable) + val csqLength = min(csq.length, maxLength - length) + length += csqLength + JsonEncoder.encode(csq, appendable, start = 0, end = csqLength) + appendMaxLengthMarker() } return this } @@ -15,14 +26,27 @@ internal class JsonEscapedAppendable( start: Int, end: Int, ): Appendable { - if (csq != null) { - JsonEncoder.encode(csq, appendable, start = start, end = end) + if (length >= maxLength) return this + if (csq != null && start < end) { + val csqLength = min(end - start, maxLength - length) + length += csqLength + JsonEncoder.encode(csq, appendable, start = start, end = start + csqLength) + appendMaxLengthMarker() } return this } override fun append(ch: Char): Appendable { + if (length >= maxLength) return this + length++ JsonEncoder.encode(ch, appendable) + appendMaxLengthMarker() return this } + + private fun appendMaxLengthMarker() { + if (length >= maxLength) { + JsonEncoder.encode(maxLengthMarker, appendable) + } + } } diff --git a/klog/src/main/kotlin/com/coditory/klog/text/json/JsonExceptionFormatter.kt b/klog/src/main/kotlin/com/coditory/klog/text/json/JsonExceptionFormatter.kt new file mode 100644 index 0000000..150e53e --- /dev/null +++ b/klog/src/main/kotlin/com/coditory/klog/text/json/JsonExceptionFormatter.kt @@ -0,0 +1,24 @@ +package com.coditory.klog.text.json + +import com.coditory.klog.text.plain.PlainTextExceptionFormatter + +fun interface JsonExceptionFormatter { + fun format( + throwable: Throwable, + appendable: Appendable, + ) + + companion object { + fun from( + formatter: PlainTextExceptionFormatter, + escape: Boolean = true, + ): JsonExceptionFormatter { + return JsonExceptionFormatter { throwable, appendable -> + appendable.append('"') + val wrapped = if (escape) JsonEscapedAppendable(appendable) else appendable + formatter.format(throwable, wrapped) + appendable.append('"') + } + } + } +} diff --git a/klog/src/main/kotlin/com/coditory/klog/text/json/JsonLogEventSerializer.kt b/klog/src/main/kotlin/com/coditory/klog/text/json/JsonLogEventSerializer.kt index 27d2c0a..958488e 100644 --- a/klog/src/main/kotlin/com/coditory/klog/text/json/JsonLogEventSerializer.kt +++ b/klog/src/main/kotlin/com/coditory/klog/text/json/JsonLogEventSerializer.kt @@ -3,6 +3,7 @@ package com.coditory.klog.text.json import com.coditory.klog.LogEvent import com.coditory.klog.LogEventField import com.coditory.klog.text.TextLogEventSerializer +import com.coditory.klog.text.plain.PlainTextMessageFormatter import com.coditory.klog.text.shared.SizedAppendable class JsonLogEventSerializer( @@ -16,13 +17,15 @@ class JsonLogEventSerializer( private val levelFormatter: JsonLevelFormatter = JsonLevelFormatter.default(), private val loggerNameFormatter: JsonStringFormatter = JsonStringFormatter.default(escape = false), private val threadFormatter: JsonStringFormatter = JsonStringFormatter.default(escape = false), - private val messageFormatter: JsonStringFormatter = JsonStringFormatter.default(maxLength = 10 * 1024), + private val messageFormatter: JsonMessageFormatter = JsonMessageFormatter.from(PlainTextMessageFormatter.messageAndException()), private val contextFormatter: JsonContextFormatter = JsonContextFormatter.default(), private val itemsFormatter: JsonMapFormatter = JsonMapFormatter.default(), + private val exceptionFormatter: JsonExceptionFormatter? = null, ) : TextLogEventSerializer { private val fieldPaths = LogEventField.all() .filter { whitelist.contains(it) } + .filter { it != LogEventField.EXCEPTION || exceptionFormatter != null } .associateWith { field -> val name = fieldNames[field] ?: fieldNameMapper?.invoke(field) ?: field.name.lowercase() val path = @@ -112,6 +115,7 @@ class JsonLogEventSerializer( LogEventField.CONTEXT -> formatContext(event, appendable) LogEventField.MESSAGE -> formatMessage(event, appendable) LogEventField.ITEMS -> formatItems(event, appendable) + LogEventField.EXCEPTION -> formatException(event, appendable) } } @@ -154,7 +158,7 @@ class JsonLogEventSerializer( event: LogEvent, appendable: Appendable, ) { - messageFormatter.format(event.message, appendable) + messageFormatter.format(event.message, event.throwable, appendable) } private fun formatItems( @@ -165,6 +169,15 @@ class JsonLogEventSerializer( itemsFormatter.format(items, appendable) } + private fun formatException( + event: LogEvent, + appendable: Appendable, + ) { + if (exceptionFormatter != null && event.throwable != null) { + exceptionFormatter.format(event.throwable, appendable) + } + } + private data class Node( val name: String, val value: LogEventField? = null, @@ -199,10 +212,4 @@ class JsonLogEventSerializer( addChild(child, idx + 1, path, value) } } - - companion object { - fun default(): JsonLogEventSerializer { - return JsonLogEventSerializer() - } - } } diff --git a/klog/src/main/kotlin/com/coditory/klog/text/json/JsonMessageFormatter.kt b/klog/src/main/kotlin/com/coditory/klog/text/json/JsonMessageFormatter.kt new file mode 100644 index 0000000..df14ec4 --- /dev/null +++ b/klog/src/main/kotlin/com/coditory/klog/text/json/JsonMessageFormatter.kt @@ -0,0 +1,30 @@ +package com.coditory.klog.text.json + +import com.coditory.klog.text.plain.PlainTextMessageFormatter + +fun interface JsonMessageFormatter { + fun format( + message: String, + throwable: Throwable?, + appendable: Appendable, + ) + + companion object { + fun from( + messageFormatter: PlainTextMessageFormatter, + escape: Boolean = true, + ): JsonMessageFormatter { + return JsonMessageFormatter { message, throwable, appendable -> + appendable.append('"') + val wrapped = + if (escape) { + JsonEscapedAppendable(appendable) + } else { + appendable + } + messageFormatter.format(message, throwable, wrapped) + appendable.append('"') + } + } + } +} diff --git a/klog/src/main/kotlin/com/coditory/klog/text/json/JsonStringFormatter.kt b/klog/src/main/kotlin/com/coditory/klog/text/json/JsonStringFormatter.kt index bf611ab..f539434 100644 --- a/klog/src/main/kotlin/com/coditory/klog/text/json/JsonStringFormatter.kt +++ b/klog/src/main/kotlin/com/coditory/klog/text/json/JsonStringFormatter.kt @@ -1,6 +1,7 @@ package com.coditory.klog.text.json import com.coditory.klog.text.plain.PlainTextStringFormatter +import com.coditory.klog.text.shared.SizedAppendable import kotlin.math.min fun interface JsonStringFormatter { @@ -13,10 +14,26 @@ fun interface JsonStringFormatter { fun default( escape: Boolean = true, maxLength: Int = Int.MAX_VALUE, + maxLengthMarker: String = "[trimmed]", ): JsonStringFormatter { return JsonStringFormatter { text, appendable -> appendable.append('"') - val wrapped = if (escape) JsonEscapedAppendable(appendable) else appendable + val wrapped = + if (escape) { + JsonEscapedAppendable( + appendable = appendable, + maxLength = maxLength, + maxLengthMarker = maxLengthMarker, + ) + } else if (maxLength < Int.MAX_VALUE) { + SizedAppendable( + appendable = appendable, + maxLength = maxLength, + maxLengthMarker = maxLengthMarker, + ) + } else { + appendable + } wrapped.append(text, 0, min(maxLength, text.length)) appendable.append('"') } diff --git a/klog/src/main/kotlin/com/coditory/klog/text/plain/PlainTextExceptionFormatter.kt b/klog/src/main/kotlin/com/coditory/klog/text/plain/PlainTextExceptionFormatter.kt new file mode 100644 index 0000000..0af6796 --- /dev/null +++ b/klog/src/main/kotlin/com/coditory/klog/text/plain/PlainTextExceptionFormatter.kt @@ -0,0 +1,59 @@ +package com.coditory.klog.text.plain + +import com.coditory.klog.text.shared.SizedAppendable.Companion.LONG_TEXT_LENGTH +import com.coditory.klog.text.shared.SizedAppendable.Companion.MAX_LENGTH_MARKER +import java.io.PrintWriter +import java.io.StringWriter + +fun interface PlainTextExceptionFormatter { + fun format( + throwable: Throwable, + appendable: Appendable, + ) + + companion object { + fun fullStackTrace( + newLine: Boolean = true, + maxLength: Int = LONG_TEXT_LENGTH, + maxLengthMarker: String = MAX_LENGTH_MARKER, + ): PlainTextExceptionFormatter { + return PlainTextExceptionFormatter { e, appendable -> + val sw = StringWriter() + val pw = PrintWriter(sw) + e.printStackTrace(pw) + val stack = sw.toString() + if (newLine) { + appendable.append('\n') + } + if (stack.length <= maxLength) { + appendable.append(stack) + } else { + val trimmed = stack.substring(0, maxLength - maxLengthMarker.length) + appendable.append(trimmed) + appendable.append(maxLengthMarker) + } + } + } + + fun messageOnly( + newLine: Boolean = false, + maxLength: Int = LONG_TEXT_LENGTH, + maxLengthMarker: String = MAX_LENGTH_MARKER, + ): PlainTextExceptionFormatter { + return PlainTextExceptionFormatter { e, appendable -> + val msg = e.message + if (!msg.isNullOrEmpty()) { + if (newLine) { + appendable.append('\n') + } + if (msg.length <= maxLength) { + appendable.append(msg) + } else { + appendable.append(msg, 0, maxLength) + appendable.append(maxLengthMarker) + } + } + } + } + } +} diff --git a/klog/src/main/kotlin/com/coditory/klog/text/plain/PlainTextLogEventSerializer.kt b/klog/src/main/kotlin/com/coditory/klog/text/plain/PlainTextLogEventSerializer.kt index 23506c2..9b418dd 100644 --- a/klog/src/main/kotlin/com/coditory/klog/text/plain/PlainTextLogEventSerializer.kt +++ b/klog/src/main/kotlin/com/coditory/klog/text/plain/PlainTextLogEventSerializer.kt @@ -17,10 +17,11 @@ class PlainTextLogEventSerializer( private val levelFormatter: PlainTextLevelFormatter = PlainTextLevelFormatter.default(), private val loggerNameFormatter: PlainTextStringFormatter = PlainTextStringFormatter.default(), private val threadFormatter: PlainTextStringFormatter = PlainTextStringFormatter.default(), - private val messageFormatter: PlainTextStringFormatter = PlainTextStringFormatter.default(), + private val messageFormatter: PlainTextMessageFormatter = PlainTextMessageFormatter.messageOnly(), private val contextFormatter: PlainTextContextFormatter = PlainTextContextFormatter.default(), private val itemsFormatter: PlainTextMapFormatter = PlainTextMapFormatter.default(), private val messageSeparator: String = ": ", + private val exceptionFormatter: PlainTextExceptionFormatter? = PlainTextExceptionFormatter.fullStackTrace(), private val mergeContextToItems: Boolean = false, ) : TextLogEventSerializer { override fun format( @@ -48,6 +49,7 @@ class PlainTextLogEventSerializer( LogEventField.CONTEXT -> formatContext(event, sized) LogEventField.MESSAGE -> formatMessage(event, sized) LogEventField.ITEMS -> formatItems(event, sized) + LogEventField.EXCEPTION -> formatException(event, sized) } } } @@ -91,7 +93,7 @@ class PlainTextLogEventSerializer( event: LogEvent, appendable: Appendable, ) { - messageFormatter.format(event.message, appendable) + messageFormatter.format(event.message, event.throwable, appendable) } private fun formatItems( @@ -102,6 +104,15 @@ class PlainTextLogEventSerializer( itemsFormatter.format(items, appendable) } + private fun formatException( + event: LogEvent, + appendable: Appendable, + ) { + if (exceptionFormatter != null && event.throwable != null) { + exceptionFormatter.format(event.throwable, appendable) + } + } + companion object { fun development(ansi: Boolean? = null): PlainTextLogEventSerializer { val ansiResolved = ansi ?: ConsoleColors.ANSI_CONSOLE @@ -117,7 +128,6 @@ class PlainTextLogEventSerializer( threadFormatter = PlainTextStringFormatter.builder().ansi(ansiResolved).style(threadStyle).prefix("[") .postfix("]").build(), - messageFormatter = PlainTextStringFormatter.default(), ) } @@ -128,7 +138,7 @@ class PlainTextLogEventSerializer( levelFormatter = PlainTextLevelFormatter.leftPadded(), loggerNameFormatter = PlainTextStringFormatter.builder().compactSections().maxLength(1024).build(), threadFormatter = PlainTextStringFormatter.builder().prefix("[").postfix("]").maxLength(1024).build(), - messageFormatter = PlainTextStringFormatter.builder().maxLength(100 * 1024).build(), + messageFormatter = PlainTextMessageFormatter.messageOnly(PlainTextStringFormatter.limitted()), ) } } diff --git a/klog/src/main/kotlin/com/coditory/klog/text/plain/PlainTextMessageFormatter.kt b/klog/src/main/kotlin/com/coditory/klog/text/plain/PlainTextMessageFormatter.kt new file mode 100644 index 0000000..37222ef --- /dev/null +++ b/klog/src/main/kotlin/com/coditory/klog/text/plain/PlainTextMessageFormatter.kt @@ -0,0 +1,29 @@ +package com.coditory.klog.text.plain + +fun interface PlainTextMessageFormatter { + fun format( + message: String, + throwable: Throwable?, + appendable: Appendable, + ) + + companion object { + fun messageAndException( + messageFormatter: PlainTextStringFormatter = PlainTextStringFormatter.limitted(), + exceptionFormatter: PlainTextExceptionFormatter = PlainTextExceptionFormatter.fullStackTrace(), + ): PlainTextMessageFormatter { + return PlainTextMessageFormatter { message, throwable, appendable -> + messageFormatter.format(message, appendable) + if (throwable != null) { + exceptionFormatter.format(throwable, appendable) + } + } + } + + fun messageOnly(messageFormatter: PlainTextStringFormatter = PlainTextStringFormatter.default()): PlainTextMessageFormatter { + return PlainTextMessageFormatter { message, _, appendable -> + messageFormatter.format(message, appendable) + } + } + } +} diff --git a/klog/src/main/kotlin/com/coditory/klog/text/plain/PlainTextStringFormatter.kt b/klog/src/main/kotlin/com/coditory/klog/text/plain/PlainTextStringFormatter.kt index 7278aa1..1e0e7fe 100644 --- a/klog/src/main/kotlin/com/coditory/klog/text/plain/PlainTextStringFormatter.kt +++ b/klog/src/main/kotlin/com/coditory/klog/text/plain/PlainTextStringFormatter.kt @@ -1,5 +1,8 @@ package com.coditory.klog.text.plain +import com.coditory.klog.text.shared.SizedAppendable.Companion.LONG_TEXT_LENGTH +import com.coditory.klog.text.shared.SizedAppendable.Companion.MAX_LENGTH_MARKER + fun interface PlainTextStringFormatter { fun format( text: String, @@ -13,6 +16,26 @@ fun interface PlainTextStringFormatter { } } + fun limitted( + maxLength: Int = LONG_TEXT_LENGTH, + maxLengthMarker: String = MAX_LENGTH_MARKER, + ): PlainTextStringFormatter { + return PlainTextStringFormatter { text, appendable -> + if (text.length <= maxLength) { + appendable.append(text) + } else { + appendable.append(text, 0, maxLength) + appendable.append(maxLengthMarker) + } + } + } + + fun from(configure: PlainTextStringFormatterBuilder.() -> Unit): PlainTextStringFormatter { + val builder = PlainTextStringFormatterBuilder() + configure(builder) + return builder.build() + } + fun builder(): PlainTextStringFormatterBuilder { return PlainTextStringFormatterBuilder() } diff --git a/klog/src/main/kotlin/com/coditory/klog/text/plain/PlainTextStringFormatterBuilder.kt b/klog/src/main/kotlin/com/coditory/klog/text/plain/PlainTextStringFormatterBuilder.kt index a737a06..c9d6e5e 100644 --- a/klog/src/main/kotlin/com/coditory/klog/text/plain/PlainTextStringFormatterBuilder.kt +++ b/klog/src/main/kotlin/com/coditory/klog/text/plain/PlainTextStringFormatterBuilder.kt @@ -5,6 +5,7 @@ import kotlin.math.max class PlainTextStringFormatterBuilder internal constructor() { private var cacheSize: Int = 0 private var maxLength: Int = Integer.MAX_VALUE + private var maxLengthMarker: String = "" private var padding: Padding? = null private var prefix: StyledText = StyledText.empty() private var postfix: StyledText = StyledText.empty() @@ -26,6 +27,11 @@ class PlainTextStringFormatterBuilder internal constructor() { return this } + fun maxLengthMarker(maxLengthMarker: String): PlainTextStringFormatterBuilder { + this.maxLengthMarker = maxLengthMarker + return this + } + fun padLeft(pad: Char = ' '): PlainTextStringFormatterBuilder { this.padding = Padding.left(pad) return this @@ -104,6 +110,7 @@ class PlainTextStringFormatterBuilder internal constructor() { } else { ConfigurablePlainTextStringFormatter( maxLength = maxLength, + maxLengthMarker = maxLengthMarker, padding = padding, prefix = prefix, postfix = postfix, @@ -127,6 +134,7 @@ class PlainTextStringFormatterBuilder internal constructor() { internal class ConfigurablePlainTextStringFormatter( private val maxLength: Int, + private val maxLengthMarker: String, private val padding: Padding?, private val prefix: StyledText, private val postfix: StyledText, @@ -151,6 +159,7 @@ internal class ConfigurablePlainTextStringFormatter( appendable.append(text) } else { appendable.append(text, 0, maxLength) + appendable.append(maxLengthMarker) } appendable.append(style.postfix) if (padding?.right() == true && text.length < maxLength) { diff --git a/klog/src/main/kotlin/com/coditory/klog/text/shared/SizedAppendable.kt b/klog/src/main/kotlin/com/coditory/klog/text/shared/SizedAppendable.kt index a6b4db1..28f6330 100644 --- a/klog/src/main/kotlin/com/coditory/klog/text/shared/SizedAppendable.kt +++ b/klog/src/main/kotlin/com/coditory/klog/text/shared/SizedAppendable.kt @@ -1,21 +1,25 @@ package com.coditory.klog.text.shared -import kotlin.math.max +import kotlin.math.min internal class SizedAppendable( private val appendable: Appendable, - initialSize: Int = 0, + private val maxLength: Int = Int.MAX_VALUE, + private val maxLengthMarker: String = MAX_LENGTH_MARKER, ) : Appendable { - private var length: Int = initialSize + private var length: Int = 0 fun length(): Int { return length } override fun append(csq: CharSequence?): Appendable { + if (length >= maxLength) return this if (csq != null) { - length += csq.length - appendable.append(csq) + val csqLength = min(csq.length, maxLength - length) + length += csqLength + appendable.append(csq, 0, csqLength) + appendMaxLengthMarker() } return this } @@ -25,16 +29,32 @@ internal class SizedAppendable( start: Int, end: Int, ): Appendable { - if (csq != null) { - length += max(0, end - start - 1) - appendable.append(csq, start, end) + if (length >= maxLength) return this + if (csq != null && start < end) { + val csqLength = min(end - start, maxLength - length) + length += csqLength + appendable.append(csq, start, start + csqLength) + appendMaxLengthMarker() } return this } override fun append(c: Char): Appendable { + if (length >= maxLength) return this length++ appendable.append(c) + appendMaxLengthMarker() return this } + + private fun appendMaxLengthMarker() { + if (length >= maxLength) { + appendable.append(maxLengthMarker) + } + } + + companion object { + const val LONG_TEXT_LENGTH: Int = 10 * 1024 + const val MAX_LENGTH_MARKER: String = " [trimmed]" + } } diff --git a/klog/src/test/kotlin/com/coditory/klog/KlogSpec.kt b/klog/src/test/kotlin/com/coditory/klog/KlogSpec.kt index a777d6c..8a7c254 100644 --- a/klog/src/test/kotlin/com/coditory/klog/KlogSpec.kt +++ b/klog/src/test/kotlin/com/coditory/klog/KlogSpec.kt @@ -1,6 +1,5 @@ package com.coditory.klog -import com.coditory.klog.config.klogConfig import com.coditory.klog.publish.InMemoryPublisher import com.coditory.klog.shared.UpdatableFixedClock import io.kotest.core.spec.style.FunSpec @@ -10,17 +9,20 @@ class KlogSpec : FunSpec({ val clock = UpdatableFixedClock() val publisher = InMemoryPublisher() val klog = - Klog( - klogConfig { - clock(clock) - stream { - blockingPublisher(publisher) - } - }, - ) + klog { + clock(clock) + stream { + blockingPublisher(publisher) + } + } + val logger = klog.logger("com.coditory.Logger") - test("should emit a basic log event") { + beforeTest { + publisher.clear() + } + + test("should emit a basic info log event") { logger.info { "Hello" } publisher.getLogs().size shouldBe 1 publisher.getLastLog() shouldBe @@ -36,4 +38,22 @@ class KlogSpec : FunSpec({ items = emptyMap(), ) } + + test("should emit a basic error log event") { + val e = IllegalArgumentException("SampleException") + logger.error(e) { "Something went wrong" } + publisher.getLogs().size shouldBe 1 + publisher.getLastLog() shouldBe + LogEvent( + priority = LogPriority.STANDARD, + timestamp = clock.zonedDateTime(), + logger = "com.coditory.Logger", + level = Level.ERROR, + thread = Thread.currentThread().name, + message = "Something went wrong", + throwable = e, + context = emptyMap(), + items = emptyMap(), + ) + } })