From 8611ebb1ea50c9f8df5995913dfa870ea3eade43 Mon Sep 17 00:00:00 2001 From: observer Date: Tue, 15 Sep 2026 03:03:15 +0100 Subject: [PATCH] Add logback extensions, coloured output, toggleable logging in IRCClient --- .gitignore | 2 +- build.gradle.kts | 5 +- .../com/shr4pnel/ferretirc/IrcClient.kt | 14 ++- .../com/shr4pnel/ferretirc/irc/Server.kt | 10 +- .../com/shr4pnel/ferretirc/net/Connection.kt | 13 ++- .../shr4pnel/ferretirc/net/MessageParser.kt | 7 +- .../shr4pnel/ferretirc/net/MessageReader.kt | 7 +- .../shr4pnel/ferretirc/net/MessageWriter.kt | 5 +- .../com/shr4pnel/ferretirc/util/Helpers.kt | 7 -- .../shr4pnel/ferretirc/util/LogbackHelpers.kt | 109 ++++++++++++++++++ src/main/resources/logback-base.xml | 29 +++++ src/main/resources/logback.xml | 10 +- .../com/shr4pnel/ferretirc/IrcClientTest.kt | 14 +-- src/test/resources/logback-test.xml | 15 +++ 14 files changed, 201 insertions(+), 46 deletions(-) delete mode 100644 src/main/kotlin/com/shr4pnel/ferretirc/util/Helpers.kt create mode 100644 src/main/kotlin/com/shr4pnel/ferretirc/util/LogbackHelpers.kt create mode 100644 src/main/resources/logback-base.xml create mode 100644 src/test/resources/logback-test.xml diff --git a/.gitignore b/.gitignore index 7570608..cfe28b4 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,6 @@ bin/ ### Mac OS ### .DS_Store -src/test/resources/logs/ngircd.log +src/test/resources/logs/*.log !src/test/resources/logs/.gitkeep .idea diff --git a/build.gradle.kts b/build.gradle.kts index f8964c1..d0f6b4d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,7 +19,8 @@ dependencies { implementation("io.ktor:ktor-network:$ktorVersion") implementation("io.ktor:ktor-network-tls:$ktorVersion") implementation("io.github.oshai:kotlin-logging-jvm:8.0.4") - implementation("ch.qos.logback:logback-classic:1.6.1") + implementation("ch.qos.logback:logback-classic:1.6.3") + testImplementation("org.jline:jansi-core:4.4.3") } kotlin { @@ -28,4 +29,6 @@ kotlin { tasks.test { useJUnitPlatform() + val logDir = layout.projectDirectory.dir("src/test/resources/logs").toString() + systemProperty("TEST_LOG_DIR", logDir) } diff --git a/src/main/kotlin/com/shr4pnel/ferretirc/IrcClient.kt b/src/main/kotlin/com/shr4pnel/ferretirc/IrcClient.kt index 9fd1dbc..d1e7c6e 100644 --- a/src/main/kotlin/com/shr4pnel/ferretirc/IrcClient.kt +++ b/src/main/kotlin/com/shr4pnel/ferretirc/IrcClient.kt @@ -1,27 +1,33 @@ package com.shr4pnel.ferretirc import com.shr4pnel.ferretirc.irc.Server -import com.shr4pnel.ferretirc.util.Helpers import com.shr4pnel.ferretirc.net.Connection import com.shr4pnel.ferretirc.net.messages.ClientMessage import com.shr4pnel.ferretirc.net.messages.ServerMessage +import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLoggingConfiguration +import io.github.oshai.kotlinlogging.slf4j.logger import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel +import org.slf4j.helpers.NOPLogger -class IrcClient(hostname: String, port: Int, enableLogging: Boolean = true) { +class IrcClient( + hostname: String, + port: Int, + var enableLogging: Boolean = false, +) { init { KotlinLoggingConfiguration.logStartupMessage = false - Helpers.enableLogging = enableLogging } + private val logger = if (enableLogging) KotlinLogging.logger {} else KotlinLogging.logger(NOPLogger.NOP_LOGGER) val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) lateinit var server: Server @PublishedApi - internal val connection = Connection(hostname, port, scope) + internal val connection = Connection(hostname, port, scope, enableLogging) /** * Connect to the socket diff --git a/src/main/kotlin/com/shr4pnel/ferretirc/irc/Server.kt b/src/main/kotlin/com/shr4pnel/ferretirc/irc/Server.kt index bb8706c..c277744 100644 --- a/src/main/kotlin/com/shr4pnel/ferretirc/irc/Server.kt +++ b/src/main/kotlin/com/shr4pnel/ferretirc/irc/Server.kt @@ -2,18 +2,22 @@ package com.shr4pnel.ferretirc.irc import com.shr4pnel.ferretirc.net.messages.ClientMessage import com.shr4pnel.ferretirc.net.messages.ServerMessage -import kotlinx.coroutines.CoroutineScope +import io.github.oshai.kotlinlogging.KotlinLogging +import io.github.oshai.kotlinlogging.slf4j.logger import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.onSubscription import kotlinx.coroutines.flow.takeWhile +import org.slf4j.helpers.NOPLogger class Server( private val msgBuffer: SharedFlow, private val outgoingMessages: Channel, - private val scope: CoroutineScope, + enableLogging: Boolean = false, ) { + private val logger = if (enableLogging) KotlinLogging.logger {} else KotlinLogging.logger(NOPLogger.NOP_LOGGER) + lateinit var features: ISupportFeatures private set @@ -21,6 +25,7 @@ class Server( private set suspend fun fetchChannels(): Set { + logger.debug { "Fetching IRC channels" } val buffer = mutableSetOf() msgBuffer @@ -29,7 +34,6 @@ class Server( .takeWhile { it !is ServerMessage.Numeric.RPL_LISTEND } .filterIsInstance() .collect { buffer.add(IRCChannel(it.chanName, it.clientCount, it.topic)) } - channels = buffer.toSortedSet(compareBy { it.chanName }) return channels } diff --git a/src/main/kotlin/com/shr4pnel/ferretirc/net/Connection.kt b/src/main/kotlin/com/shr4pnel/ferretirc/net/Connection.kt index 632651b..009e21d 100644 --- a/src/main/kotlin/com/shr4pnel/ferretirc/net/Connection.kt +++ b/src/main/kotlin/com/shr4pnel/ferretirc/net/Connection.kt @@ -7,7 +7,12 @@ import io.ktor.network.sockets.aSocket import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -class Connection(val hostname: String, val port: Int, val scope: CoroutineScope) { +class Connection( + val hostname: String, + val port: Int, + val scope: CoroutineScope, + val enableLogging: Boolean = false, +) { private val selectorManager = SelectorManager(Dispatchers.IO) private val socketBuilder = aSocket(selectorManager).tcp() lateinit var socket: Socket @@ -16,10 +21,10 @@ class Connection(val hostname: String, val port: Int, val scope: CoroutineScope) suspend fun connect(): Server { socket = socketBuilder.connect(hostname, port) - reader = MessageReader(socket, scope) - writer = MessageWriter(socket, scope) + reader = MessageReader(socket, scope, enableLogging) + writer = MessageWriter(socket, scope, enableLogging) reader.start() writer.start() - return Server(reader.sharedMessageBuffer, writer.outgoingMessages, scope) + return Server(reader.sharedMessageBuffer, writer.outgoingMessages, enableLogging) } } diff --git a/src/main/kotlin/com/shr4pnel/ferretirc/net/MessageParser.kt b/src/main/kotlin/com/shr4pnel/ferretirc/net/MessageParser.kt index 6d05fb0..7f65868 100644 --- a/src/main/kotlin/com/shr4pnel/ferretirc/net/MessageParser.kt +++ b/src/main/kotlin/com/shr4pnel/ferretirc/net/MessageParser.kt @@ -1,15 +1,14 @@ package com.shr4pnel.ferretirc.net import com.shr4pnel.ferretirc.net.messages.ServerMessage -import com.shr4pnel.ferretirc.util.Helpers import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.slf4j.logger import kotlinx.coroutines.channels.Channel import org.slf4j.helpers.NOPLogger -class MessageParser(val incoming: Channel) { +class MessageParser(val incoming: Channel, enableLogging: Boolean = false) { val incomingParsedMessages = Channel() - private val logger = if (Helpers.enableLogging) KotlinLogging.logger {} else KotlinLogging.logger(NOPLogger.NOP_LOGGER) + private val logger = if (enableLogging) KotlinLogging.logger {} else KotlinLogging.logger(NOPLogger.NOP_LOGGER) class MessageBuilder { private var tags: String? = null @@ -154,7 +153,7 @@ class MessageParser(val incoming: Channel) { suspend fun start() { val builder = MessageBuilder() for (msg in incoming) { - logger.trace { "Receiving: $msg" } + logger.debug { "Receiving: $msg" } incomingParsedMessages.send(builder.build(msg)) } } diff --git a/src/main/kotlin/com/shr4pnel/ferretirc/net/MessageReader.kt b/src/main/kotlin/com/shr4pnel/ferretirc/net/MessageReader.kt index d26ec5d..29f0d3d 100644 --- a/src/main/kotlin/com/shr4pnel/ferretirc/net/MessageReader.kt +++ b/src/main/kotlin/com/shr4pnel/ferretirc/net/MessageReader.kt @@ -2,7 +2,6 @@ package com.shr4pnel.ferretirc.net import com.shr4pnel.ferretirc.net.messages.MessageIO import com.shr4pnel.ferretirc.net.messages.ServerMessage -import com.shr4pnel.ferretirc.util.Helpers import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.slf4j.logger import io.ktor.network.sockets.Socket @@ -20,12 +19,12 @@ import org.slf4j.helpers.NOPLogger import kotlin.reflect.KClass import kotlin.reflect.cast -class MessageReader(socket: Socket, scope: CoroutineScope) : MessageIO(socket, scope) { +class MessageReader(socket: Socket, scope: CoroutineScope, enableLogging: Boolean = false) : MessageIO(socket, scope) { private lateinit var receive: ByteReadChannel private val messageBuffer = MutableSharedFlow(16, 64) val sharedMessageBuffer = messageBuffer.asSharedFlow() - val parser = MessageParser(incomingMessages) - private val logger = if (Helpers.enableLogging) KotlinLogging.logger {} else KotlinLogging.logger(NOPLogger.NOP_LOGGER) + val parser = MessageParser(incomingMessages, enableLogging) + private val logger = if (enableLogging) KotlinLogging.logger {} else KotlinLogging.logger(NOPLogger.NOP_LOGGER) init { scope.launch { diff --git a/src/main/kotlin/com/shr4pnel/ferretirc/net/MessageWriter.kt b/src/main/kotlin/com/shr4pnel/ferretirc/net/MessageWriter.kt index 754b863..f94eca9 100644 --- a/src/main/kotlin/com/shr4pnel/ferretirc/net/MessageWriter.kt +++ b/src/main/kotlin/com/shr4pnel/ferretirc/net/MessageWriter.kt @@ -1,7 +1,6 @@ package com.shr4pnel.ferretirc.net import com.shr4pnel.ferretirc.net.messages.MessageIO -import com.shr4pnel.ferretirc.util.Helpers import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.slf4j.logger import io.ktor.network.sockets.Socket @@ -12,9 +11,9 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import org.slf4j.helpers.NOPLogger -class MessageWriter(socket: Socket, scope: CoroutineScope) : MessageIO(socket, scope) { +class MessageWriter(socket: Socket, scope: CoroutineScope, enableLogging: Boolean = false) : MessageIO(socket, scope) { private lateinit var send: ByteWriteChannel - private val logger = if (Helpers.enableLogging) KotlinLogging.logger {} else KotlinLogging.logger(NOPLogger.NOP_LOGGER) + private val logger = if (enableLogging) KotlinLogging.logger {} else KotlinLogging.logger(NOPLogger.NOP_LOGGER) override fun start() = scope.launch { diff --git a/src/main/kotlin/com/shr4pnel/ferretirc/util/Helpers.kt b/src/main/kotlin/com/shr4pnel/ferretirc/util/Helpers.kt deleted file mode 100644 index 5a1af7f..0000000 --- a/src/main/kotlin/com/shr4pnel/ferretirc/util/Helpers.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.shr4pnel.ferretirc.util - -class Helpers { - companion object LoggingConfig { - var enableLogging = true - } -} diff --git a/src/main/kotlin/com/shr4pnel/ferretirc/util/LogbackHelpers.kt b/src/main/kotlin/com/shr4pnel/ferretirc/util/LogbackHelpers.kt new file mode 100644 index 0000000..909e9f0 --- /dev/null +++ b/src/main/kotlin/com/shr4pnel/ferretirc/util/LogbackHelpers.kt @@ -0,0 +1,109 @@ +package com.shr4pnel.ferretirc.util + +import ch.qos.logback.classic.Level +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.pattern.CompositeConverter +import ch.qos.logback.core.pattern.color.BoldWhiteCompositeConverter +import ch.qos.logback.core.pattern.color.ForegroundCompositeConverterBase +import ch.qos.logback.core.pattern.color.ANSIConstants as ANSI + +private fun center(severity: String, width: Int = 7): String { + val pad = width - severity.length + if (pad <= 0) return severity + val leading = pad / 2 + return buildString { + repeat(leading) { append(" ") } + append(severity) + repeat(pad - leading) { append(" ") } + } +} + +/** 8 bit ANSI codes (logback uses 3/4 bit which isn't always equivalent) */ +object Codes { + const val SET_DEFAULT_COLOR = ANSI.ESC_START + ANSI.RESET + ANSI.DEFAULT_FG + ANSI.ESC_END + const val FG_START = ANSI.ESC_START + "38;5;" + const val FG_BOLD_START = ANSI.ESC_START + ANSI.BOLD + "38;5;" + const val BG_START = ANSI.ESC_START + "48;5;" + const val RED = "9" + const val BLACK = "16" + const val GREEN = "22" + const val BLUE = "27" + const val ORANGE = "202" + const val DIMMED_WHITE = "250" + const val WHITE = "255" + const val UNSET = "" +} + +/** + * Interface of HighlightingCompositeConverterExt, used to colour the background and foreground of log messages + * depending on their severity. + * + * Inheritor of ForegroundCompositeConverterBase, the implementation controlling colour-based transformations of + * log events + * + * @see HighlightingCompositeConverterExt + * @see CompositeConverter + */ +abstract class ForegroundBackgroundCompositeConverter : ForegroundCompositeConverterBase() { + override fun transform(event: E, `in`: String) = buildString { + val fg = getForegroundColorCode(event) + val bg = getBackgroundColourCode(event) + val severity = center(`in`.trim()) + if (fg.isNotEmpty()) { + append(Codes.FG_BOLD_START) + append(fg) + append(ANSI.ESC_END) + } + if (bg.isNotEmpty()) { + append(Codes.BG_START) + append(getBackgroundColourCode(event)) + append(ANSI.ESC_END) + } + append(severity) + append(Codes.SET_DEFAULT_COLOR) + } + + abstract fun getBackgroundColourCode(event: ILoggingEvent): String +} + +/** + * Converter class extension for control over backgrounds and foregrounds< + * + * Overrides methods of ForegroundBackgroundCompositeConverter to specify the bg/fg colours depending on severity + * + * Adapted from [shuwada/logback-custom-color](https://github.com/shuwada/logback-custom-color) + */ +class HighlightingCompositeConverterExt : ForegroundBackgroundCompositeConverter() { + override fun getForegroundColorCode(event: ILoggingEvent) = when (event.level) { + Level.TRACE -> Codes.WHITE + Level.DEBUG -> Codes.WHITE + Level.WARN -> Codes.WHITE + Level.INFO -> Codes.WHITE + Level.ERROR -> Codes.WHITE + else -> Codes.UNSET + } + + override fun getBackgroundColourCode(event: ILoggingEvent) = when (event.level) { + Level.TRACE -> Codes.BLACK + Level.DEBUG -> Codes.GREEN + Level.WARN -> Codes.ORANGE + Level.INFO -> Codes.BLUE + Level.ERROR -> Codes.RED + else -> Codes.UNSET + } +} + +/** + * Converter rule which pulls out the last string of the thread split by spaces + */ +class ThreadConverter : CompositeConverter() { + override fun transform(event: E, `in`: String) = `in`.split(" ").last() +} + +/** + * Converter rule which converts text to bold white + */ +class BoldWhiteCompositeConverterExt : BoldWhiteCompositeConverter() { + override fun transform(event: E, `in`: String) = + Codes.FG_BOLD_START + Codes.DIMMED_WHITE + ANSI.ESC_END + `in` + Codes.SET_DEFAULT_COLOR +} diff --git a/src/main/resources/logback-base.xml b/src/main/resources/logback-base.xml new file mode 100644 index 0000000..2b7f3f9 --- /dev/null +++ b/src/main/resources/logback-base.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + ${STDOUT_LEVEL:-debug} + + + %d{HH:mm:ss.SSS} %highlightext(%level) %-13.13threadconvert(%t) %-36.36logger %boldwhiteext(%msg) %n + + + + ${TEST_LOG_DIR}/libferretirc.log + true + false + + %d{HH:mm:ss.SSS} [%.16t/%-5level] %logger %msg%n + + + \ No newline at end of file diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 7741021..67af514 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -2,14 +2,8 @@ - - - - - %d{ISO8601} [%5level] %.36logger %msg%n - - - + + \ No newline at end of file diff --git a/src/test/kotlin/com/shr4pnel/ferretirc/IrcClientTest.kt b/src/test/kotlin/com/shr4pnel/ferretirc/IrcClientTest.kt index 5c55609..34ab354 100644 --- a/src/test/kotlin/com/shr4pnel/ferretirc/IrcClientTest.kt +++ b/src/test/kotlin/com/shr4pnel/ferretirc/IrcClientTest.kt @@ -4,7 +4,7 @@ import com.shr4pnel.ferretirc.net.messages.ServerMessage import io.github.oshai.kotlinlogging.KotlinLogging import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withTimeoutOrNull +import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test @@ -20,7 +20,7 @@ class IrcClientTest { companion object { lateinit var processes: MutableList - val client = IrcClient("localhost", 6667) + val client = IrcClient("localhost", 6667, enableLogging = true) private val logger = KotlinLogging.logger {} @JvmStatic @@ -42,7 +42,10 @@ class IrcClientTest { ), ) client.connect() - client.register("shr4p", "Tyler D", "password") + client.register("shr4p", "Tyler Fullname", "password") + guestUser.connect() + guestUser.register("guest", "guest", "pass") + client.server } @JvmStatic @@ -103,10 +106,7 @@ class IrcClientTest { @Test fun messageLands() = - runBlocking { - val guestUser = IrcClient("localhost", 6667, false) - guestUser.connect() - guestUser.register("guest", "guest", "pass") + runTest { val channels = client.server.fetchChannels() val message = Message.PrivMsg(channels.first(), "bring me to life") guestUser.queueMessage(message) diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml new file mode 100644 index 0000000..70ba500 --- /dev/null +++ b/src/test/resources/logback-test.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file