Browse Source

[console/logging] slf4j binding support

Karlatemp 2 years ago
parent
commit
6c295723db

+ 1 - 1
buildSrc/src/main/kotlin/Versions.kt

@@ -60,7 +60,7 @@ object Versions {
     const val shadow = "7.1.3-mirai-modified-SNAPSHOT"
     const val shadow = "7.1.3-mirai-modified-SNAPSHOT"
 
 
     const val logback = "1.2.5"
     const val logback = "1.2.5"
-    const val slf4j = "1.7.32"
+    const val slf4j = "2.0.3"
     const val log4j = "2.17.2"
     const val log4j = "2.17.2"
     const val asm = "9.4"
     const val asm = "9.4"
     const val difflib = "1.3.0"
     const val difflib = "1.3.0"

+ 1 - 0
mirai-console/backend/mirai-console/build.gradle.kts

@@ -61,6 +61,7 @@ dependencies {
     smartImplementation(`maven-resolver-impl`)
     smartImplementation(`maven-resolver-impl`)
     smartImplementation(`maven-resolver-connector-basic`)
     smartImplementation(`maven-resolver-connector-basic`)
     smartImplementation(`maven-resolver-transport-http`)
     smartImplementation(`maven-resolver-transport-http`)
+    smartImplementation(`slf4j-api`)
     smartApi(`kotlinx-coroutines-jdk8`)
     smartApi(`kotlinx-coroutines-jdk8`)
 
 
     testApi(project(":mirai-core"))
     testApi(project(":mirai-core"))

+ 1 - 0
mirai-console/backend/mirai-console/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider

@@ -0,0 +1 @@
+net.mamoe.mirai.console.internal.logging.externalbind.slf4j.MiraiConsoleSLF4JService

+ 5 - 0
mirai-console/backend/mirai-console/src/internal/MiraiConsoleImplementationBridge.kt

@@ -42,6 +42,7 @@ import net.mamoe.mirai.console.internal.extension.GlobalComponentStorage
 import net.mamoe.mirai.console.internal.extension.GlobalComponentStorageImpl
 import net.mamoe.mirai.console.internal.extension.GlobalComponentStorageImpl
 import net.mamoe.mirai.console.internal.logging.LoggerControllerImpl
 import net.mamoe.mirai.console.internal.logging.LoggerControllerImpl
 import net.mamoe.mirai.console.internal.logging.MiraiConsoleLogger
 import net.mamoe.mirai.console.internal.logging.MiraiConsoleLogger
+import net.mamoe.mirai.console.internal.logging.externalbind.slf4j.MiraiConsoleSLF4JAdapter
 import net.mamoe.mirai.console.internal.permission.BuiltInPermissionService
 import net.mamoe.mirai.console.internal.permission.BuiltInPermissionService
 import net.mamoe.mirai.console.internal.plugin.PluginManagerImpl
 import net.mamoe.mirai.console.internal.plugin.PluginManagerImpl
 import net.mamoe.mirai.console.internal.shutdown.ShutdownDaemon
 import net.mamoe.mirai.console.internal.shutdown.ShutdownDaemon
@@ -278,6 +279,10 @@ ___  ____           _   _____                       _
             }
             }
         }
         }
 
 
+        phase("initialize logging bridges") {
+            MiraiConsoleSLF4JAdapter.doSlf4JInit()
+        }
+
         phase("initialize all plugins") {
         phase("initialize all plugins") {
             pluginManager // init
             pluginManager // init
 
 

+ 3 - 0
mirai-console/backend/mirai-console/src/internal/data/builtins/LoggerConfig.kt

@@ -38,11 +38,14 @@ public class LoggerConfig : ReadOnlyPluginConfig("Logger") {
             "example.logger" to AbstractLoggerController.LogPriority.NONE,
             "example.logger" to AbstractLoggerController.LogPriority.NONE,
             "console.debug" to AbstractLoggerController.LogPriority.NONE,
             "console.debug" to AbstractLoggerController.LogPriority.NONE,
             "Bot" to AbstractLoggerController.LogPriority.ALL,
             "Bot" to AbstractLoggerController.LogPriority.ALL,
+            "org.eclipse.aether.internal" to AbstractLoggerController.LogPriority.INFO,
+            "org.apache.http.wire" to AbstractLoggerController.LogPriority.INFO,
         )
         )
     )
     )
 
 
     @Serializable
     @Serializable
     public class Binding @MiraiExperimentalApi public constructor(
     public class Binding @MiraiExperimentalApi public constructor(
+        public val slf4j: Boolean = true,
     )
     )
 
 
     @ValueDescription(
     @ValueDescription(

+ 98 - 0
mirai-console/backend/mirai-console/src/internal/logging/externalbind/slf4j/MiraiConsoleSLF4JService.kt

@@ -0,0 +1,98 @@
+/*
+ * Copyright 2019-2022 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.console.internal.logging.externalbind.slf4j
+
+import net.mamoe.mirai.console.MiraiConsoleImplementation.ConsoleDataScope.Companion.get
+import net.mamoe.mirai.console.internal.data.builtins.DataScope
+import net.mamoe.mirai.console.internal.data.builtins.LoggerConfig
+import net.mamoe.mirai.utils.MiraiLogger
+import net.mamoe.mirai.utils.safeCast
+import org.slf4j.ILoggerFactory
+import org.slf4j.IMarkerFactory
+import org.slf4j.event.SubstituteLoggingEvent
+import org.slf4j.helpers.BasicMarkerFactory
+import org.slf4j.helpers.NOPLoggerFactory
+import org.slf4j.helpers.NOPMDCAdapter
+import org.slf4j.helpers.SubstituteLoggerFactory
+import org.slf4j.spi.MDCAdapter
+import org.slf4j.spi.SLF4JServiceProvider
+
+@PublishedApi
+internal class MiraiConsoleSLF4JService : SLF4JServiceProvider {
+    private val basicMarkerFactory = BasicMarkerFactory()
+    private val nopMDCAdapter = NOPMDCAdapter()
+    private val dfactory = ILoggerFactory { MiraiConsoleSLF4JAdapter.getCurrentLogFactory().getLogger(it) }
+
+    override fun getMarkerFactory(): IMarkerFactory = basicMarkerFactory
+    override fun getMDCAdapter(): MDCAdapter = nopMDCAdapter
+    override fun getRequestedApiVersion(): String = "2.0"
+    override fun getLoggerFactory(): ILoggerFactory = dfactory
+    override fun initialize() {}
+}
+
+internal object MiraiConsoleSLF4JAdapter {
+    /**
+     * Used before mirai-console start
+     */
+    private val substituteServiceFactory = SubstituteLoggerFactory()
+
+    @Volatile
+    private var initialized: Boolean = false
+
+    @Volatile
+    private var currentLoggerFactory: ILoggerFactory = substituteServiceFactory
+
+    internal fun getCurrentLogFactory(): ILoggerFactory {
+        if (initialized) return currentLoggerFactory
+
+        synchronized(MiraiConsoleSLF4JAdapter::class.java) {
+            return currentLoggerFactory
+        }
+    }
+
+    internal fun doSlf4JInit() {
+        synchronized(MiraiConsoleSLF4JAdapter::class.java) {
+            val logConfig = DataScope.get<LoggerConfig>()
+
+            currentLoggerFactory = if (logConfig.binding.slf4j) {
+                ILoggerFactory { ident ->
+                    SLF4JAdapterLogger(MiraiLogger.Factory.create(MiraiConsoleSLF4JAdapter::class.java, ident))
+                }
+            } else {
+                NOPLoggerFactory()
+            }
+            initialized = true
+
+            // region relay events
+
+            substituteServiceFactory.postInitialization()
+            substituteServiceFactory.loggers.forEach { slog ->
+                slog.setDelegate(currentLoggerFactory.getLogger(slog.name))
+            }
+
+            substituteServiceFactory.eventQueue.let { queue ->
+                for (event in queue) {
+                    replaySingleEvent(event)
+                }
+            }
+            substituteServiceFactory.clear()
+            // endregion
+        }
+    }
+
+
+    private fun replaySingleEvent(event: SubstituteLoggingEvent?) {
+        if (event == null) return
+        val substLogger = event.logger
+
+        substLogger.delegate().safeCast<SLF4JAdapterLogger>()?.process(event)
+    }
+
+}

+ 304 - 0
mirai-console/backend/mirai-console/src/internal/logging/externalbind/slf4j/SLF4JAdapterLogger.kt

@@ -0,0 +1,304 @@
+/*
+ * Copyright 2019-2022 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.console.internal.logging.externalbind.slf4j
+
+import net.mamoe.mirai.utils.MiraiLogger
+import org.slf4j.Logger
+import org.slf4j.Marker
+import org.slf4j.event.SubstituteLoggingEvent
+import java.lang.invoke.MethodHandle
+import java.lang.invoke.MethodHandles
+import java.lang.invoke.MethodType
+import java.nio.CharBuffer
+import java.text.MessageFormat
+import java.util.regex.Pattern
+import org.slf4j.event.Level as SLF4JEventLevel
+
+@Suppress("RegExpRedundantEscape")
+internal class SLF4JAdapterLogger(
+    private val logger: MiraiLogger
+) : Logger {
+    // Copied from Log4J
+    internal companion object {
+        private const val FORMAT_SPECIFIER = "%(\\d+\\$)?([-#+ 0,(\\<]*)?(\\d+)?(\\.\\d+)?([tT])?([a-zA-Z%])"
+
+        private val MSG_PATTERN = Pattern.compile(FORMAT_SPECIFIER)
+        private const val DELIM_START = '{'
+        private const val DELIM_STOP = '}'
+        private const val ESCAPE_CHAR = '\\'
+
+        @JvmStatic
+        internal fun String.simpleFormat(args: Array<out Any?>): String {
+            val buffer = StringBuilder()
+            val reader = CharBuffer.wrap(this)
+            var isEscape = false
+            var index = 0
+            while (reader.hasRemaining()) {
+                when (val next = reader.get()) {
+                    ESCAPE_CHAR -> {
+                        if (isEscape) {
+                            buffer.append(ESCAPE_CHAR)
+                        }
+                        isEscape = !isEscape
+                    }
+                    DELIM_START -> {
+                        if (isEscape) {
+                            buffer.append(next)
+                        } else {
+                            if (reader.hasRemaining()) {
+                                if (reader.get(reader.position()) == DELIM_STOP) {
+                                    reader.get()
+                                    buffer.append(args.getOrNull(index))
+                                    index++
+                                } else {
+                                    buffer.append(DELIM_START)
+                                }
+                            } else {
+                                buffer.append(DELIM_START)
+                            }
+                        }
+                        isEscape = false
+                    }
+                    else -> buffer.append(next).also { isEscape = false }
+                }
+            }
+            return buffer.toString()
+        }
+
+        internal fun String.format1(vararg arguments: Any?): String = format2(arguments)
+
+        // (java.lang.String, java.lang.Object[]): java.lang.String
+        @Suppress("LocalVariableName")
+        private val formatWithLog4JMH: MethodHandle? = kotlin.runCatching {
+            val c_ParameterizedMessage = Class.forName("org.apache.logging.log4j.message.ParameterizedMessage")
+
+            val mhLookup = MethodHandles.lookup()
+            val mh_newParameterizedMessage = mhLookup.findConstructor(
+                c_ParameterizedMessage, MethodType.methodType(Void.TYPE, String::class.java, Array<Any>::class.java)
+            ).asFixedArity()
+
+            val mh_getFormattedMessage = mhLookup.findVirtual(
+                c_ParameterizedMessage, "getFormattedMessage", MethodType.methodType(String::class.java)
+            )
+
+            MethodHandles.filterReturnValue(mh_newParameterizedMessage, mh_getFormattedMessage)
+
+        }.getOrNull()
+
+        @JvmStatic
+        internal fun String.format2(args: Array<out Any?>): String {
+            kotlin.runCatching {
+                val formatter = MessageFormat(this)
+                val formats = formatter.formats
+                if (formats.isNotEmpty()) {
+                    return formatter.format(args)
+                }
+            }
+            kotlin.runCatching {
+                if (MSG_PATTERN.matcher(this).find()) {
+                    return String.format(this, *args)
+                }
+            }
+            kotlin.runCatching {
+                // Try format with Log4J
+                formatWithLog4JMH?.let { formatWithLog4JMH ->
+                    return formatWithLog4JMH.invoke(this@format2, args) as String
+                }
+            }
+            return simpleFormat(args)
+        }
+    }
+
+    //////////////////////////////////////////////////////////////////
+    override fun isTraceEnabled(): Boolean = logger.isVerboseEnabled
+    override fun isTraceEnabled(marker: Marker?): Boolean = logger.isVerboseEnabled
+    override fun isDebugEnabled(): Boolean = logger.isDebugEnabled
+    override fun isDebugEnabled(marker: Marker?): Boolean = logger.isDebugEnabled
+    override fun isInfoEnabled(): Boolean = logger.isInfoEnabled
+    override fun isInfoEnabled(marker: Marker?): Boolean = logger.isInfoEnabled
+    override fun isWarnEnabled(): Boolean = logger.isWarningEnabled
+    override fun isWarnEnabled(marker: Marker?): Boolean = logger.isWarningEnabled
+    override fun isErrorEnabled(): Boolean = logger.isErrorEnabled
+    override fun isErrorEnabled(marker: Marker?): Boolean = logger.isErrorEnabled
+    //////////////////////////////////////////////////////////////////
+
+    override fun getName(): String = logger.identity ?: "<unknown>"
+
+    @Suppress("DuplicatedCode")
+    internal fun process(event: SubstituteLoggingEvent) {
+        val msg = event.message
+        val argx = event.argumentArray
+        val throwx = event.throwable
+
+        val evtlv = event.level ?: return
+
+        val isEnabled = when (evtlv) {
+            SLF4JEventLevel.ERROR -> isErrorEnabled
+            SLF4JEventLevel.WARN -> isWarnEnabled
+            SLF4JEventLevel.INFO -> isInfoEnabled
+            SLF4JEventLevel.DEBUG -> isDebugEnabled
+            SLF4JEventLevel.TRACE -> isTraceEnabled
+        }
+        if (!isEnabled) return
+
+        if (argx == null) {
+            when (evtlv) {
+                SLF4JEventLevel.ERROR -> error(msg, t = throwx)
+                SLF4JEventLevel.WARN -> warn(msg, t = throwx)
+                SLF4JEventLevel.INFO -> info(msg, t = throwx)
+                SLF4JEventLevel.DEBUG -> debug(msg, t = throwx)
+                SLF4JEventLevel.TRACE -> trace(msg, t = throwx)
+            }
+            return
+        }
+
+        if (throwx == null) {
+            when (evtlv) {
+                SLF4JEventLevel.ERROR -> error(msg, arguments = argx)
+                SLF4JEventLevel.WARN -> warn(msg, arguments = argx)
+                SLF4JEventLevel.INFO -> info(msg, arguments = argx)
+                SLF4JEventLevel.DEBUG -> debug(msg, arguments = argx)
+                SLF4JEventLevel.TRACE -> trace(msg, arguments = argx)
+            }
+            return
+        }
+
+        val msg2 = msg.format2(argx)
+        when (evtlv) {
+            SLF4JEventLevel.ERROR -> error(msg2, t = throwx)
+            SLF4JEventLevel.WARN -> warn(msg2, t = throwx)
+            SLF4JEventLevel.INFO -> info(msg2, t = throwx)
+            SLF4JEventLevel.DEBUG -> debug(msg2, t = throwx)
+            SLF4JEventLevel.TRACE -> trace(msg2, t = throwx)
+        }
+    }
+
+    //////////////////////////////////////////////////////////////////
+
+    private inline fun iT(a: () -> Unit) {
+        if (isTraceEnabled) a()
+    }
+
+    private inline fun iD(a: () -> Unit) {
+        if (isDebugEnabled) a()
+    }
+
+    private inline fun iI(a: () -> Unit) {
+        if (isInfoEnabled) a()
+    }
+
+    private inline fun iW(a: () -> Unit) {
+        if (isWarnEnabled) a()
+    }
+
+    private inline fun iE(a: () -> Unit) {
+        if (isErrorEnabled) a()
+    }
+
+    //////////////////////////////////////////////////////////////////
+
+    override fun trace(msg: String?) {
+        logger.verbose(msg)
+    }
+
+    override fun trace(msg: String?, t: Throwable?) {
+        logger.verbose(msg, t)
+    }
+
+    override fun trace(format: String, arg: Any?) = iT { trace(format.format1(arg)) }
+    override fun trace(format: String, arg1: Any?, arg2: Any?) = iT { trace(format.format1(arg1, arg2)) }
+    override fun trace(format: String, arguments: Array<out Any?>) = iT { trace(format.format2(arguments)) }
+
+    override fun trace(marker: Marker?, msg: String?) = trace(msg)
+    override fun trace(marker: Marker?, format: String, arg: Any?) = trace(format, arg)
+    override fun trace(marker: Marker?, format: String, arg1: Any?, arg2: Any?) = trace(format, arg1, arg2)
+    override fun trace(marker: Marker?, format: String, argArray: Array<out Any?>) = trace(format, argArray)
+    override fun trace(marker: Marker?, msg: String?, t: Throwable?) = trace(msg, t)
+
+    //////////////////////////////////////////////////////////////////
+
+    override fun debug(msg: String?) {
+        logger.debug(msg)
+    }
+
+    override fun debug(msg: String?, t: Throwable?) {
+        logger.debug(msg, t)
+    }
+
+    override fun debug(format: String, arg: Any?) = iD { debug(format.format1(arg)) }
+    override fun debug(format: String, arg1: Any?, arg2: Any?) = iD { debug(format.format1(arg1, arg2)) }
+    override fun debug(format: String, arguments: Array<out Any?>) = iD { debug(format.format2(arguments)) }
+
+    override fun debug(marker: Marker?, msg: String?) = debug(msg)
+    override fun debug(marker: Marker?, format: String, arg: Any?) = debug(format, arg)
+    override fun debug(marker: Marker?, format: String, arg1: Any?, arg2: Any?) = debug(format, arg1, arg2)
+    override fun debug(marker: Marker?, format: String, arguments: Array<out Any?>) = debug(format, arguments)
+    override fun debug(marker: Marker?, msg: String?, t: Throwable?) = debug(msg, t)
+
+    //////////////////////////////////////////////////////////////////
+
+    override fun info(msg: String?) {
+        logger.info(msg)
+    }
+
+    override fun info(msg: String?, t: Throwable?) {
+        logger.info(msg, t)
+    }
+
+    override fun info(format: String, arg: Any?) = iI { info(format.format1(arg)) }
+    override fun info(format: String, arg1: Any?, arg2: Any?) = iI { info(format.format1(arg1, arg2)) }
+    override fun info(format: String, arguments: Array<out Any?>) = iI { info(format.format2(arguments)) }
+
+    override fun info(marker: Marker?, msg: String?) = info(msg)
+    override fun info(marker: Marker?, format: String, arg: Any?) = info(format, arg)
+    override fun info(marker: Marker?, format: String, arg1: Any?, arg2: Any?) = info(format, arg1, arg2)
+    override fun info(marker: Marker?, format: String, arguments: Array<out Any?>) = info(format, arguments)
+    override fun info(marker: Marker?, msg: String?, t: Throwable?) = info(msg, t)
+
+    //////////////////////////////////////////////////////////////////
+
+    override fun warn(msg: String?) {
+        logger.warning(msg)
+    }
+
+    override fun warn(msg: String?, t: Throwable?) {
+        logger.warning(msg, t)
+    }
+
+    override fun warn(format: String, arg: Any?) = iW { warn(format.format1(arg)) }
+    override fun warn(format: String, arguments: Array<out Any?>) = iW { warn(format.format2(arguments)) }
+    override fun warn(format: String, arg1: Any?, arg2: Any?) = iW { warn(format.format1(arg1, arg2)) }
+
+    override fun warn(marker: Marker?, msg: String?) = warn(msg)
+    override fun warn(marker: Marker?, format: String, arg: Any?) = warn(format, arg)
+    override fun warn(marker: Marker?, format: String, arg1: Any?, arg2: Any?) = warn(format, arg1, arg2)
+    override fun warn(marker: Marker?, format: String, arguments: Array<out Any?>) = warn(format, arguments)
+    override fun warn(marker: Marker?, msg: String?, t: Throwable?) = warn(msg, t)
+
+    //////////////////////////////////////////////////////////////////
+
+    override fun error(msg: String?) {
+        logger.error(msg)
+    }
+
+    override fun error(msg: String?, t: Throwable?) {
+        logger.error(msg, t)
+    }
+
+    override fun error(format: String, arg: Any?) = iE { error(format.format1(arg)) }
+    override fun error(format: String, arg1: Any?, arg2: Any?) = iE { error(format.format1(arg1, arg2)) }
+    override fun error(format: String, arguments: Array<out Any?>) = iE { error(format.format2(arguments)) }
+
+    override fun error(marker: Marker?, msg: String?) = error(msg)
+    override fun error(marker: Marker?, format: String, arg: Any?) = error(format, arg)
+    override fun error(marker: Marker?, format: String, arg1: Any?, arg2: Any?) = error(format, arg1, arg2)
+    override fun error(marker: Marker?, format: String, arguments: Array<out Any?>) = error(format, arguments)
+    override fun error(marker: Marker?, msg: String?, t: Throwable?) = error(msg, t)
+}

+ 6 - 1
mirai-console/backend/mirai-console/src/internal/plugin/JvmPluginClassLoader.kt

@@ -129,7 +129,12 @@ internal class DynLibClassLoader : DynamicClasspathClassLoader {
             if (name in AllDependenciesClassesHolder.allclasses) {
             if (name in AllDependenciesClassesHolder.allclasses) {
                 return AllDependenciesClassesHolder.appClassLoader.loadClass(name)
                 return AllDependenciesClassesHolder.appClassLoader.loadClass(name)
             }
             }
-            if (name.startsWith("net.mamoe.mirai.") || name.startsWith("kotlin.") || name.startsWith("kotlinx.")) { // Avoid plugin classing cheating
+            if (
+                name.startsWith("net.mamoe.mirai.")
+                || name.startsWith("kotlin.")
+                || name.startsWith("kotlinx.")
+                || name.startsWith("org.slf4j.")
+            ) { // Avoid plugin classing cheating
                 try {
                 try {
                     return AllDependenciesClassesHolder.appClassLoader.loadClass(name)
                     return AllDependenciesClassesHolder.appClassLoader.loadClass(name)
                 } catch (ignored: ClassNotFoundException) {
                 } catch (ignored: ClassNotFoundException) {

+ 5 - 0
mirai-console/backend/mirai-console/src/internal/plugin/JvmPluginDependencyDownload.kt

@@ -80,6 +80,11 @@ internal class JvmPluginDependencyDownloader(
             ) return@DependencyFilter false
             ) return@DependencyFilter false
         }
         }
 
 
+        // Re-download slf4j-api is unnecessary since slf4j-api was bound by console
+        if (artGroup == "org.slf4j" && artId == "slf4j-api") {
+            return@DependencyFilter false
+        }
+
         // Loaded by console system
         // Loaded by console system
         if ("$artGroup:$artId" in MiraiConsoleBuildDependencies.dependencies)
         if ("$artGroup:$artId" in MiraiConsoleBuildDependencies.dependencies)
             return@DependencyFilter false
             return@DependencyFilter false