From b887a8df9e2ad39ab660f886307741754860c2bf Mon Sep 17 00:00:00 2001 From: Adam <152864218+adam-enko@users.noreply.github.com> Date: Thu, 17 Oct 2024 15:14:52 +0200 Subject: [PATCH] Forward Dokka Generator messages to Gradle logger (#3833) * Forward Dokka Generator messages to Gradle logger --- .../src/main/kotlin/internal/LoggerAdapter.kt | 34 ++- .../main/kotlin/tasks/DokkaGenerateTask.kt | 1 + .../kotlin/workers/DokkaGeneratorWorker.kt | 16 +- .../test/kotlin/internal/LoggerAdapterTest.kt | 78 +++++++ .../kotlin/DokkaGeneratorLoggingTest.kt | 221 ++++++++++++++++++ 5 files changed, 342 insertions(+), 8 deletions(-) create mode 100644 dokka-runners/dokka-gradle-plugin/src/test/kotlin/internal/LoggerAdapterTest.kt create mode 100644 dokka-runners/dokka-gradle-plugin/src/testFunctional/kotlin/DokkaGeneratorLoggingTest.kt diff --git a/dokka-runners/dokka-gradle-plugin/src/main/kotlin/internal/LoggerAdapter.kt b/dokka-runners/dokka-gradle-plugin/src/main/kotlin/internal/LoggerAdapter.kt index 704d44fb99..856ed85d3e 100644 --- a/dokka-runners/dokka-gradle-plugin/src/main/kotlin/internal/LoggerAdapter.kt +++ b/dokka-runners/dokka-gradle-plugin/src/main/kotlin/internal/LoggerAdapter.kt @@ -5,20 +5,30 @@ package org.jetbrains.dokka.gradle.internal import org.jetbrains.dokka.utilities.DokkaLogger import org.jetbrains.dokka.utilities.LoggingLevel +import org.jetbrains.dokka.utilities.LoggingLevel.* +import org.slf4j.Logger import java.io.File import java.io.Writer import java.util.concurrent.atomic.AtomicInteger /** - * Logs all Dokka messages to a file. + * A logger for [org.jetbrains.dokka.DokkaGenerator]. * + * All messages will be written to [logWriter] and forwarded to a Gradle [logger] (at an appropriate log level). + * + * [org.jetbrains.dokka.DokkaGenerator] makes heavy use of coroutines and parallelization, + * so use thread-safe practices when handling logging messages. + * + * @param logTag Prepend all [logger] messages with this tag. * @see org.jetbrains.dokka.DokkaGenerator */ // Gradle causes OOM errors when there is a lot of console output. Logging to file is a workaround. // https://github.com/gradle/gradle/issues/23965 // https://github.com/gradle/gradle/issues/15621 internal class LoggerAdapter( - outputFile: File + outputFile: File, + private val logger: Logger, + private val logTag: String, ) : DokkaLogger, AutoCloseable { private val logWriter: Writer @@ -43,22 +53,32 @@ internal class LoggerAdapter( get() = errorsCounter.get() set(value) = errorsCounter.set(value) - override fun debug(message: String) = log(LoggingLevel.DEBUG, message) - override fun progress(message: String) = log(LoggingLevel.PROGRESS, message) - override fun info(message: String) = log(LoggingLevel.INFO, message) + override fun debug(message: String) = log(DEBUG, message) + override fun progress(message: String) = log(PROGRESS, message) + override fun info(message: String) = log(INFO, message) override fun warn(message: String) { warningsCount++ - log(LoggingLevel.WARN, message) + log(WARN, message) } override fun error(message: String) { errorsCount++ - log(LoggingLevel.ERROR, message) + log(ERROR, message) } @Synchronized private fun log(level: LoggingLevel, message: String) { + when (level) { + PROGRESS, + INFO -> logger.info("[$logTag] " + message.prependIndent().trimStart()) + + DEBUG -> logger.debug("[$logTag] " + message.prependIndent().trimStart()) + + WARN -> logger.warn("w: [$logTag] " + message.prependIndent().trimStart()) + + ERROR -> logger.error("e: [$logTag] " + message.prependIndent().trimStart()) + } logWriter.appendLine("[${level.name}] $message") } diff --git a/dokka-runners/dokka-gradle-plugin/src/main/kotlin/tasks/DokkaGenerateTask.kt b/dokka-runners/dokka-gradle-plugin/src/main/kotlin/tasks/DokkaGenerateTask.kt index 8a0e9e003a..a3c3a03a19 100644 --- a/dokka-runners/dokka-gradle-plugin/src/main/kotlin/tasks/DokkaGenerateTask.kt +++ b/dokka-runners/dokka-gradle-plugin/src/main/kotlin/tasks/DokkaGenerateTask.kt @@ -143,6 +143,7 @@ constructor( workQueue.submit(DokkaGeneratorWorker::class) { this.dokkaParameters.set(dokkaConfiguration) this.logFile.set(workerLogFile) + this.taskPath.set(this@DokkaGenerateTask.path) } } diff --git a/dokka-runners/dokka-gradle-plugin/src/main/kotlin/workers/DokkaGeneratorWorker.kt b/dokka-runners/dokka-gradle-plugin/src/main/kotlin/workers/DokkaGeneratorWorker.kt index bbf605563c..00a8c26635 100644 --- a/dokka-runners/dokka-gradle-plugin/src/main/kotlin/workers/DokkaGeneratorWorker.kt +++ b/dokka-runners/dokka-gradle-plugin/src/main/kotlin/workers/DokkaGeneratorWorker.kt @@ -4,6 +4,8 @@ package org.jetbrains.dokka.gradle.workers import org.gradle.api.file.RegularFileProperty +import org.gradle.api.logging.Logger +import org.gradle.api.logging.Logging import org.gradle.api.provider.Property import org.gradle.workers.WorkAction import org.gradle.workers.WorkParameters @@ -27,6 +29,12 @@ abstract class DokkaGeneratorWorker : WorkAction val logFile: RegularFileProperty + + /** + * The [org.gradle.api.Task.getPath] of the task that invokes this worker. + * Only used in log messages. + */ + val taskPath: Property } override fun execute() { @@ -56,7 +64,11 @@ abstract class DokkaGeneratorWorker : WorkAction + LoggerAdapter( + logFile, + logger, + logTag = parameters.taskPath.get(), + ).use { logger -> logger.progress("Executing DokkaGeneratorWorker with dokkaParameters: $dokkaParameters") val generator = DokkaGenerator(dokkaParameters, logger) @@ -69,6 +81,8 @@ abstract class DokkaGeneratorWorker : WorkAction Unit): Duration = diff --git a/dokka-runners/dokka-gradle-plugin/src/test/kotlin/internal/LoggerAdapterTest.kt b/dokka-runners/dokka-gradle-plugin/src/test/kotlin/internal/LoggerAdapterTest.kt new file mode 100644 index 0000000000..b5b9963749 --- /dev/null +++ b/dokka-runners/dokka-gradle-plugin/src/test/kotlin/internal/LoggerAdapterTest.kt @@ -0,0 +1,78 @@ +package internal + +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.matchers.shouldBe +import org.jetbrains.dokka.gradle.internal.LoggerAdapter +import org.slf4j.event.SubstituteLoggingEvent +import org.slf4j.helpers.NOPLogger +import org.slf4j.helpers.SubstituteLoggerFactory +import kotlin.io.path.createTempFile +import kotlin.io.path.readText + +class LoggerAdapterTest : FunSpec({ + + test("slf4j logger output") { + val logFactory = SubstituteLoggerFactory() + + val loggerAdapter = LoggerAdapter( + createTempFile().toFile(), + logFactory.getLogger("test"), + "LOG-TAG" + ) + + loggerAdapter.error("an error msg") + loggerAdapter.warn("a warn msg") + loggerAdapter.debug("a debug msg") + loggerAdapter.info("an info msg") + loggerAdapter.progress("a progress msg") + + loggerAdapter.errorsCount shouldBe 1 + loggerAdapter.warningsCount shouldBe 1 + + logFactory.eventQueue.map { it.render() }.shouldContainExactly( + "ERROR e: [LOG-TAG] an error msg", + "WARN w: [LOG-TAG] a warn msg", + "DEBUG [LOG-TAG] a debug msg", + "INFO [LOG-TAG] an info msg", + "INFO [LOG-TAG] a progress msg", + ) + } + + test("logfile output") { + val logFile = createTempFile() + + LoggerAdapter( + logFile.toFile(), + NOPLogger.NOP_LOGGER, + "LOG-TAG" + ).use { loggerAdapter -> + loggerAdapter.error("an error msg") + loggerAdapter.warn("a warn msg") + loggerAdapter.debug("a debug msg") + loggerAdapter.info("an info msg") + loggerAdapter.progress("a progress msg") + + loggerAdapter.errorsCount shouldBe 1 + loggerAdapter.warningsCount shouldBe 1 + } + + logFile.readText() shouldBe """ + |[ERROR] an error msg + |[WARN] a warn msg + |[DEBUG] a debug msg + |[INFO] an info msg + |[PROGRESS] a progress msg + | + """.trimMargin() + } + +}) { + companion object { + private fun SubstituteLoggingEvent.render(): String = buildString { + append("$level") + append(" ") + append(message) + } + } +} diff --git a/dokka-runners/dokka-gradle-plugin/src/testFunctional/kotlin/DokkaGeneratorLoggingTest.kt b/dokka-runners/dokka-gradle-plugin/src/testFunctional/kotlin/DokkaGeneratorLoggingTest.kt new file mode 100644 index 0000000000..b6c413b203 --- /dev/null +++ b/dokka-runners/dokka-gradle-plugin/src/testFunctional/kotlin/DokkaGeneratorLoggingTest.kt @@ -0,0 +1,221 @@ +/* + * Copyright 2014-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +package org.jetbrains.dokka.gradle + +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.collections.shouldContainAll +import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.string.shouldNotContain +import org.jetbrains.dokka.gradle.internal.DokkaConstants.DOKKA_VERSION +import org.jetbrains.dokka.gradle.utils.* + +class DokkaGeneratorLoggingTest : FunSpec({ + + context("DokkaGenerator logging:") { + val project = createProject() + + test("at lifecycle log level expect only error and warn logs") { + + project.runner + .addArguments( + ":dokkaGenerateModuleHtml", + "--rerun", + ) + .build { + output.invariantNewlines() shouldContain """ + > Task :dokkaGenerateModuleHtml + e: [:dokkaGenerateModuleHtml] test error message + w: [:dokkaGenerateModuleHtml] test warn message + """.trimIndent() + + output.shouldNotContainAnyOf( + "test info message", + "test debug message", + "test progress message", + ) + } + + project.runner + .addArguments( + ":dokkaGeneratePublicationHtml", + "--rerun", + ) + .build { + output.invariantNewlines() shouldContain """ + > Task :dokkaGeneratePublicationHtml + e: [:dokkaGeneratePublicationHtml] test error message + w: [:dokkaGeneratePublicationHtml] test warn message + """.trimIndent() + + output.shouldNotContainAnyOf( + "test info message", + "test debug message", + "test progress message", + ) + } + } + + test("at info log level expect all non-debug DokkaGenerator logs") { + + project.runner + .addArguments( + ":dokkaGenerateModuleHtml", + "--rerun", + "--info", + ) + .build { + output.invariantNewlines() shouldContain """ + e: [:dokkaGenerateModuleHtml] test error message + w: [:dokkaGenerateModuleHtml] test warn message + [:dokkaGenerateModuleHtml] test info message + [:dokkaGenerateModuleHtml] test progress message + """.trimIndent() + + output shouldNotContain "test debug message" + } + + project.runner + .addArguments( + ":dokkaGeneratePublicationHtml", + "--rerun", + "--info", + ) + .build { + output.invariantNewlines() shouldContain """ + e: [:dokkaGeneratePublicationHtml] test error message + w: [:dokkaGeneratePublicationHtml] test warn message + [:dokkaGeneratePublicationHtml] test info message + [:dokkaGeneratePublicationHtml] test progress message + """.trimIndent() + + output shouldNotContain "test debug message" + } + } + + test("at debug log level expect all DokkaGenerator logs") { + + project.runner + .addArguments( + ":dokkaGenerateModuleHtml", + "--rerun", + "--debug", + ) + .build { + output + .lineSequence() + .filter { "[:dokkaGenerateModuleHtml]" in it } + .map { it.substringAfter(" ") } // drop the timestamp from the log message + .toList() + .shouldContainAll( + "[ERROR] [org.jetbrains.dokka.gradle.workers.DokkaGeneratorWorker] e: [:dokkaGenerateModuleHtml] test error message", + "[WARN] [org.jetbrains.dokka.gradle.workers.DokkaGeneratorWorker] w: [:dokkaGenerateModuleHtml] test warn message", + "[DEBUG] [org.jetbrains.dokka.gradle.workers.DokkaGeneratorWorker] [:dokkaGenerateModuleHtml] test debug message", + "[INFO] [org.jetbrains.dokka.gradle.workers.DokkaGeneratorWorker] [:dokkaGenerateModuleHtml] test info message", + "[INFO] [org.jetbrains.dokka.gradle.workers.DokkaGeneratorWorker] [:dokkaGenerateModuleHtml] test progress message", + ) + } + + project.runner + .addArguments( + ":dokkaGeneratePublicationHtml", + "--rerun", + "--debug", + ) + .build { + output + .lineSequence() + .filter { "[:dokkaGeneratePublicationHtml]" in it } + .map { it.substringAfter(" ") } // drop the timestamp from the log message + .toList() + .shouldContainAll( + "[ERROR] [org.jetbrains.dokka.gradle.workers.DokkaGeneratorWorker] e: [:dokkaGeneratePublicationHtml] test error message", + "[WARN] [org.jetbrains.dokka.gradle.workers.DokkaGeneratorWorker] w: [:dokkaGeneratePublicationHtml] test warn message", + "[DEBUG] [org.jetbrains.dokka.gradle.workers.DokkaGeneratorWorker] [:dokkaGeneratePublicationHtml] test debug message", + "[INFO] [org.jetbrains.dokka.gradle.workers.DokkaGeneratorWorker] [:dokkaGeneratePublicationHtml] test info message", + "[INFO] [org.jetbrains.dokka.gradle.workers.DokkaGeneratorWorker] [:dokkaGeneratePublicationHtml] test progress message", + ) + } + } + } +}) + +/** + * Create a test project with a custom [org.jetbrains.dokka.plugability.DokkaPlugin] that logs some test messages. + */ +private fun createProject(): GradleProjectTest = gradleKtsProjectTest("dokka-generator-logging") { + + buildGradleKts = """ + |import org.jetbrains.dokka.gradle.tasks.* + | + |plugins { + | kotlin("jvm") version embeddedKotlinVersion + | id("org.jetbrains.dokka") version "$DOKKA_VERSION" + |} + | + |dependencies { + | dokkaPlugin(project(":dokka-logger-test-plugin")) + |} + | + """.trimMargin() + + settingsGradleKts += """ + |include(":dokka-logger-test-plugin") + | + """.trimMargin() + + createKotlinFile("src/main/kotlin/Foo.kt", "class Foo") + + dir("dokka-logger-test-plugin") { + buildGradleKts = """ + |plugins { + | kotlin("jvm") + |} + | + |dependencies { + | compileOnly("org.jetbrains.dokka:dokka-core:$DOKKA_VERSION") + | compileOnly("org.jetbrains.dokka:dokka-base:$DOKKA_VERSION") + |} + """.trimMargin() + + createKotlinFile( + "src/main/kotlin/DokkaLoggerTestPlugin.kt", """ + |package logtest + | + |import org.jetbrains.dokka.* + |import org.jetbrains.dokka.plugability.* + |import org.jetbrains.dokka.validity.* + | + |class DokkaLoggerTestPlugin : DokkaPlugin() { + | + | @DokkaPluginApiPreview + | override fun pluginApiPreviewAcknowledgement() = PluginApiPreviewAcknowledgement + | + | internal val logSomeMessages by extending { + | CoreExtensions.preGenerationCheck providing ::LogSomeMessages + | } + |} + | + |class LogSomeMessages(private val context: DokkaContext) : PreGenerationChecker { + | + | override fun invoke(): PreGenerationCheckerOutput { + | + | context.logger.error("test error message") + | context.logger.warn("test warn message") + | context.logger.debug("test debug message") + | context.logger.info("test info message") + | context.logger.progress("test progress message") + | + | return PreGenerationCheckerOutput(true, emptyList()) + | } + |} + | + """.trimMargin() + ) + + createFile( + "src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin", + "logtest.DokkaLoggerTestPlugin", + ) + } +}