Skip to content

Commit 9d5bb91

Browse files
committed
Add non-public %logHandler magic for log redirection
1 parent 112b543 commit 9d5bb91

File tree

7 files changed

+129
-21
lines changed

7 files changed

+129
-21
lines changed

jupyter-lib/shared-compiler/src/main/kotlin/org/jetbrains/kotlinx/jupyter/magics/AbstractMagicsHandler.kt

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ abstract class AbstractMagicsHandler : MagicsHandler {
1818
ReplLineMagic.USE_LATEST_DESCRIPTORS to ::handleUseLatestDescriptors,
1919
ReplLineMagic.OUTPUT to ::handleOutput,
2020
ReplLineMagic.LOG_LEVEL to ::handleLogLevel,
21+
ReplLineMagic.LOG_HANDLER to ::handleLogHandler,
2122
)
2223

2324
override fun handle(magic: ReplLineMagic, arg: String?, tryIgnoreErrors: Boolean, parseOnly: Boolean) {
@@ -43,4 +44,5 @@ abstract class AbstractMagicsHandler : MagicsHandler {
4344
open fun handleUseLatestDescriptors() {}
4445
open fun handleOutput() {}
4546
open fun handleLogLevel() {}
47+
open fun handleLogHandler() {}
4648
}

kotlin-jupyter-plugin/common-dependencies/src/main/kotlin/org/jetbrains/kotlinx/jupyter/common/ReplLineMagic.kt

+3-2
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ enum class ReplLineMagic(val desc: String, val argumentsUsage: String? = null, v
77
DUMP_CLASSES_FOR_SPARK("stores compiled repl classes in special folder for Spark integration", visibleInHelp = false),
88
USE_LATEST_DESCRIPTORS("use latest versions of library descriptors available. By default, bundled descriptors are used", "-[on|off]"),
99
OUTPUT("output capturing settings", "--max-cell-size=1000 --no-stdout --max-time=100 --max-buffer=400"),
10-
LOG_LEVEL("set logging level", "[off|error|warn|info|debug]");
10+
LOG_LEVEL("set logging level", "[off|error|warn|info|debug]"),
11+
LOG_HANDLER("manage logging handlers", "[list | remove <name> | add <name> --<type> [... typeArgs]]", visibleInHelp = false);
1112

1213
val nameForUser = getNameForUser(name)
1314

1415
companion object {
15-
private val names = values().map { it.nameForUser to it }.toMap()
16+
private val names = values().associateBy { it.nameForUser }
1617

1718
fun valueOfOrNull(name: String): ReplLineMagic? = names[name]
1819
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package org.jetbrains.kotlinx.jupyter
2+
3+
import ch.qos.logback.classic.Level
4+
import ch.qos.logback.classic.Logger
5+
import ch.qos.logback.classic.encoder.PatternLayoutEncoder
6+
import ch.qos.logback.classic.spi.ILoggingEvent
7+
import ch.qos.logback.core.Appender
8+
import ch.qos.logback.core.OutputStreamAppender
9+
import org.slf4j.LoggerFactory
10+
11+
object LoggingManagement {
12+
private val rootLogger = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME) as? Logger
13+
14+
private val basicEncoder = run {
15+
val encoder = PatternLayoutEncoder()
16+
encoder.context = rootLogger?.loggerContext
17+
encoder.pattern = "%-4relative [%thread] %-5level %logger{35} - %msg %n"
18+
encoder.start()
19+
encoder
20+
}
21+
22+
fun setRootLoggingLevel(level: Level) {
23+
rootLogger?.level = level
24+
}
25+
26+
fun disableLogging() = setRootLoggingLevel(Level.OFF)
27+
28+
fun mainLoggerLevel(): Level {
29+
val mainLogger = rootLogger ?: return Level.DEBUG
30+
return mainLogger.effectiveLevel
31+
}
32+
33+
fun allLogAppenders(): List<Appender<ILoggingEvent>> {
34+
val mainLogger = rootLogger ?: return emptyList()
35+
val result = mutableListOf<Appender<ILoggingEvent>>()
36+
mainLogger.iteratorForAppenders().forEachRemaining { result.add(it) }
37+
return result
38+
}
39+
40+
fun addAppender(name: String, appender: Appender<ILoggingEvent>) {
41+
appender.name = name
42+
appender.context = rootLogger?.loggerContext
43+
(appender as? OutputStreamAppender)?.encoder = basicEncoder
44+
appender.start()
45+
rootLogger?.addAppender(appender)
46+
}
47+
48+
fun removeAppender(appenderName: String) {
49+
rootLogger?.detachAppender(appenderName)
50+
}
51+
}

src/main/kotlin/org/jetbrains/kotlinx/jupyter/config.kt

-16
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package org.jetbrains.kotlinx.jupyter
22

3-
import ch.qos.logback.classic.Level
43
import jupyter.kotlin.JavaRuntime
54
import kotlinx.serialization.KSerializer
65
import kotlinx.serialization.Serializable
@@ -28,21 +27,6 @@ const val protocolVersion = "5.3"
2827

2928
internal val log by lazy { getLogger() }
3029

31-
fun setLevelForAllLoggers(level: Level) {
32-
val mainLogger = log as? ch.qos.logback.classic.Logger ?: return
33-
val allLoggers = mainLogger.loggerContext.loggerList
34-
allLoggers.forEach { logger ->
35-
logger.level = level
36-
}
37-
}
38-
39-
fun disableLogging() = setLevelForAllLoggers(Level.OFF)
40-
41-
fun mainLoggerLevel(): Level {
42-
val mainLogger = log as? ch.qos.logback.classic.Logger ?: return Level.DEBUG
43-
return mainLogger.effectiveLevel
44-
}
45-
4630
val defaultRuntimeProperties by lazy {
4731
RuntimeKernelProperties(readResourceAsIniFile("runtime.properties"))
4832
}

src/main/kotlin/org/jetbrains/kotlinx/jupyter/magics/FullMagicsHandler.kt

+51-2
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,26 @@
11
package org.jetbrains.kotlinx.jupyter.magics
22

33
import ch.qos.logback.classic.Level
4+
import ch.qos.logback.classic.spi.ILoggingEvent
5+
import ch.qos.logback.core.Appender
6+
import ch.qos.logback.core.FileAppender
47
import com.github.ajalt.clikt.core.CliktCommand
58
import com.github.ajalt.clikt.parameters.options.default
69
import com.github.ajalt.clikt.parameters.options.flag
710
import com.github.ajalt.clikt.parameters.options.option
811
import com.github.ajalt.clikt.parameters.types.int
912
import com.github.ajalt.clikt.parameters.types.long
1013
import org.jetbrains.kotlinx.jupyter.ExecutedCodeLogging
14+
import org.jetbrains.kotlinx.jupyter.LoggingManagement.addAppender
15+
import org.jetbrains.kotlinx.jupyter.LoggingManagement.allLogAppenders
16+
import org.jetbrains.kotlinx.jupyter.LoggingManagement.removeAppender
17+
import org.jetbrains.kotlinx.jupyter.LoggingManagement.setRootLoggingLevel
1118
import org.jetbrains.kotlinx.jupyter.OutputConfig
1219
import org.jetbrains.kotlinx.jupyter.ReplOptions
1320
import org.jetbrains.kotlinx.jupyter.exceptions.ReplException
1421
import org.jetbrains.kotlinx.jupyter.libraries.DefaultInfoSwitch
1522
import org.jetbrains.kotlinx.jupyter.libraries.LibrariesProcessor
1623
import org.jetbrains.kotlinx.jupyter.libraries.ResolutionInfoSwitcher
17-
import org.jetbrains.kotlinx.jupyter.setLevelForAllLoggers
1824

1925
class FullMagicsHandler(
2026
private val repl: ReplOptions,
@@ -80,6 +86,49 @@ class FullMagicsHandler(
8086
"debug" -> Level.DEBUG
8187
else -> throw ReplException("Unknown log level: '$levelStr'")
8288
}
83-
setLevelForAllLoggers(level)
89+
setRootLoggingLevel(level)
90+
}
91+
92+
override fun handleLogHandler() {
93+
val commandArgs = arg?.split(Regex("""\s+""")).orEmpty()
94+
val command = commandArgs.firstOrNull() ?: throw ReplException("Log handler command has not been passed")
95+
when (command) {
96+
"list" -> {
97+
println("Log appenders:")
98+
allLogAppenders().forEach {
99+
println(
100+
buildString {
101+
append(it.name)
102+
append(" of type ")
103+
append(it::class.simpleName)
104+
if (it is FileAppender) {
105+
append("(${it.file})")
106+
}
107+
}
108+
)
109+
}
110+
}
111+
"add" -> {
112+
val appenderName = commandArgs.getOrNull(1) ?: throw ReplException("Log handler add command needs appender name argument")
113+
val appenderType = commandArgs.getOrNull(2) ?: throw ReplException("Log handler add command needs appender type argument")
114+
val appenderTypeArgs = commandArgs.subList(3, commandArgs.size)
115+
116+
val appender: Appender<ILoggingEvent> = when (appenderType) {
117+
"--file" -> {
118+
val fileName = appenderTypeArgs.getOrNull(0) ?: throw ReplException("File appender needs file name to be specified")
119+
val res = FileAppender<ILoggingEvent>()
120+
res.file = fileName
121+
res
122+
}
123+
else -> throw ReplException("Unknown appender type: $appenderType")
124+
}
125+
addAppender(appenderName, appender)
126+
}
127+
"remove" -> {
128+
val appenderName = commandArgs.getOrNull(1) ?: throw ReplException("Log handler remove command needs appender name argument")
129+
removeAppender(appenderName)
130+
}
131+
else -> throw ReplException("")
132+
}
84133
}
85134
}

src/main/kotlin/org/jetbrains/kotlinx/jupyter/protocol.kt

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import kotlinx.serialization.json.JsonObject
88
import kotlinx.serialization.json.encodeToJsonElement
99
import org.jetbrains.annotations.TestOnly
1010
import org.jetbrains.kotlin.config.KotlinCompilerVersion
11+
import org.jetbrains.kotlinx.jupyter.LoggingManagement.disableLogging
12+
import org.jetbrains.kotlinx.jupyter.LoggingManagement.mainLoggerLevel
1113
import org.jetbrains.kotlinx.jupyter.api.DisplayResult
1214
import org.jetbrains.kotlinx.jupyter.api.KotlinKernelVersion.Companion.toMaybeUnspecifiedString
1315
import org.jetbrains.kotlinx.jupyter.api.MutableJsonObject

src/test/kotlin/org/jetbrains/kotlinx/jupyter/test/executeTests.kt

+20-1
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,21 @@ import org.jetbrains.kotlinx.jupyter.IsCompleteReply
1616
import org.jetbrains.kotlinx.jupyter.IsCompleteRequest
1717
import org.jetbrains.kotlinx.jupyter.JupyterSockets
1818
import org.jetbrains.kotlinx.jupyter.KernelStatus
19+
import org.jetbrains.kotlinx.jupyter.LoggingManagement.mainLoggerLevel
1920
import org.jetbrains.kotlinx.jupyter.Message
2021
import org.jetbrains.kotlinx.jupyter.MessageType
2122
import org.jetbrains.kotlinx.jupyter.StatusReply
2223
import org.jetbrains.kotlinx.jupyter.StreamResponse
2324
import org.jetbrains.kotlinx.jupyter.compiler.util.EvaluatedSnippetMetadata
2425
import org.jetbrains.kotlinx.jupyter.jsonObject
25-
import org.jetbrains.kotlinx.jupyter.mainLoggerLevel
2626
import org.junit.jupiter.api.Assertions.assertEquals
2727
import org.junit.jupiter.api.Assertions.assertNull
2828
import org.junit.jupiter.api.Test
2929
import org.junit.jupiter.api.Timeout
3030
import org.junit.jupiter.api.parallel.Execution
3131
import org.junit.jupiter.api.parallel.ExecutionMode
3232
import org.zeromq.ZMQ
33+
import java.io.File
3334
import java.net.URLClassLoader
3435
import java.nio.file.Files
3536
import java.util.concurrent.TimeUnit
@@ -332,4 +333,22 @@ class ExecuteTests : KernelServerTestsBase() {
332333
assertEquals("incomplete", doIsComplete("fun f() : Int { return 1"))
333334
assertEquals(if (runInSeparateProcess) DEBUG else OFF, mainLoggerLevel())
334335
}
336+
337+
@Test
338+
fun testLoggerAppender() {
339+
val file = File.createTempFile("kotlin-jupyter-logger-appender-test", ".txt")
340+
doExecute("%logHandler add f1 --file ${file.absolutePath}", false)
341+
val result1 = doExecute("2 + 2")
342+
assertEquals(jsonObject("text/plain" to "4"), result1)
343+
344+
doExecute("%logHandler remove f1", false)
345+
val result2 = doExecute("3 + 4")
346+
assertEquals(jsonObject("text/plain" to "7"), result2)
347+
348+
val logText = file.readText()
349+
assertTrue("2 + 2" in logText)
350+
assertTrue("3 + 4" !in logText)
351+
352+
file.delete()
353+
}
335354
}

0 commit comments

Comments
 (0)