Skip to content

Commit

Permalink
Merge pull request #118 from zeldigas/feature/loggin_improvements
Browse files Browse the repository at this point in the history
Loggin improvements
  • Loading branch information
zeldigas authored Nov 5, 2023
2 parents 625d7ff + c3252bc commit 708e73d
Show file tree
Hide file tree
Showing 27 changed files with 1,064 additions and 203 deletions.
12 changes: 7 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

- now in `upload` and `export-to-md` commands you can enable logging of http requests/responses and configure request
timeout
- now configuration file can be named as `text2confl.yml` or `text2confl.yaml` in addition to dot-prefixed
names (`.text2confl.yml`, `.text2confl.yaml`).

### Changed

- dependency updates:
- migrated to `io.github.oshai:kotlin-logging-jvm`
- plantuml to 1.2023.12
- migrated to `io.github.oshai:kotlin-logging-jvm`
- plantuml to 1.2023.12

### Fixed

- Non-local links detection (may cause crash on Windows)
Expand All @@ -38,8 +40,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

- `cli` model is split into 2: `core` and `cli`. Contributed by @dgautier.
- dependency updates
- Asciidoctor diagram to 2.2.13
- plantuml to 1.2023.11
- Asciidoctor diagram to 2.2.13
- plantuml to 1.2023.11

## 0.13.0 - 2023-08-28

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@ internal interface WithConfluenceServerOptions {
requestTimeout = httpRequestTimeout
)
fun askForSecret(prompt: String, requireConfirmation: Boolean = true): String?

fun configureRequestLoggingIfEnabled() {
if (httpLogLevel != LogLevel.NONE) {
enableHttpLogging()
}
}
}

internal interface WithConversionOptions {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.github.ajalt.mordant.terminal.StringPrompt
import com.github.zeldigas.text2confl.convert.ConversionFailedException
import com.github.zeldigas.text2confl.convert.FileDoesNotExistException
import com.github.zeldigas.text2confl.core.ContentValidationFailedException
import com.github.zeldigas.text2confl.core.upload.ContentUploadException
import com.github.zeldigas.text2confl.core.upload.InvalidTenantException

fun parameterMissing(what: String, cliOption: String, fileOption: String): Nothing {
Expand All @@ -31,27 +32,32 @@ fun RawOption.optionalFlag(vararg secondaryNames: String): NullableOption<Boolea

fun tryHandleException(ex: Exception): Nothing {
when (ex) {
is InvalidTenantException -> throw PrintMessage(ex.message!!, printError = true)
is FileDoesNotExistException -> throw PrintMessage(ex.message!!, printError = true)
is InvalidTenantException -> error(ex.message!!)
is FileDoesNotExistException -> error(ex.message!!)
is ContentUploadException -> error(ex.message!!)
is ConversionFailedException -> {
val reason = buildString {
append(ex.message)
if (ex.cause != null) {
append(" (cause: ${ex.cause})")
}
}
throw PrintMessage("Failed to convert ${ex.file}: $reason", printError = true)
error("Failed to convert ${ex.file}: $reason")
}

is ContentValidationFailedException -> {
val issues = ex.errors.mapIndexed { index, error -> "${index + 1}. $error" }.joinToString(separator = "\n")
throw PrintMessage("Some pages content is invalid:\n${issues}", printError = true)
error("Some pages content is invalid:\n${issues}")
}

else -> throw ex
}
}

private fun error(message: String): Nothing {
throw PrintMessage(message, printError = true, statusCode = 1)
}

fun CliktCommand.promptForSecret(prompt: String, requireConfirmation: Boolean): String? {
return if (requireConfirmation) {
ConfirmationPrompt.create(prompt, "Repeat for confirmation: ") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class DumpToMarkdown : CliktCommand(name = "export-to-md", help = "Exports confl

override fun run() {
try {
configureRequestLoggingIfEnabled()
runBlocking { dumpPage() }
} catch (ex: Exception) {
tryHandleException(ex)
Expand Down
52 changes: 52 additions & 0 deletions cli/src/main/kotlin/com/github/zeldigas/text2confl/cli/Logging.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.github.zeldigas.text2confl.cli

import ch.qos.logback.classic.Level
import ch.qos.logback.classic.Logger
import ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME
import ch.qos.logback.classic.spi.ILoggingEvent
import ch.qos.logback.core.filter.Filter
import ch.qos.logback.core.spi.FilterReply
import org.slf4j.LoggerFactory


class StdOutFilter : Filter<ILoggingEvent>() {
override fun decide(event: ILoggingEvent): FilterReply {
return if (event.level.isGreaterOrEqual(Level.WARN)) {
FilterReply.DENY
} else {
FilterReply.ACCEPT
}
}
}

class StdErrFilter : Filter<ILoggingEvent>() {
override fun decide(event: ILoggingEvent): FilterReply {
return if (event.level.isGreaterOrEqual(Level.WARN)) {
FilterReply.ACCEPT
} else {
FilterReply.DENY
}
}
}

fun configureLogging(verbosity: Int) {
if (verbosity == 0) return

val rootLogger = LoggerFactory.getLogger(ROOT_LOGGER_NAME) as Logger
rootLogger.level = when (verbosity) {
1 -> rootLogger.level
2 -> Level.INFO
else -> Level.DEBUG
}

val text2conflRoot = LoggerFactory.getLogger("com.github.zeldigas.text2confl") as Logger
text2conflRoot.level = when (verbosity) {
1 -> Level.INFO
else -> Level.DEBUG
}
}

fun enableHttpLogging() {
val rootLogger = LoggerFactory.getLogger("io.ktor.client") as Logger
rootLogger.level = Level.INFO
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package com.github.zeldigas.text2confl.cli
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.context
import com.github.ajalt.clikt.core.subcommands
import com.github.ajalt.clikt.parameters.options.counted
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.sources.ChainedValueSource
import com.github.ajalt.clikt.sources.PropertiesValueSource
import com.github.zeldigas.text2confl.core.ServiceProviderImpl
Expand All @@ -23,7 +25,10 @@ class ConfluencePublisher : CliktCommand() {
}
}

val verbosityLevel by option("-v", help = "Enable verbose output").counted()

override fun run() {
configureLogging(verbosityLevel)
currentContext.obj = ServiceProviderImpl()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import com.github.ajalt.mordant.rendering.TextColors.*
import com.github.zeldigas.confclient.model.ConfluencePage
import com.github.zeldigas.text2confl.convert.Page
import com.github.zeldigas.text2confl.core.upload.*
import io.ktor.http.*
import java.util.concurrent.atomic.AtomicLong

class PrintingUploadOperationsTracker(
val server: Url,
val prefix: String = "",
val printer: (msg: String) -> Unit = ::println
) : UploadOperationTracker {

companion object {
const val UILINK = "tinyui"
}

private val updatedCount = AtomicLong(0L)


override fun pageUpdated(
pageResult: PageOperationResult,
labelUpdate: LabelsUpdateResult,
attachmentsUpdated: AttachmentsUpdateResult
) {
if (pageResult is PageOperationResult.NotModified
&& labelUpdate == LabelsUpdateResult.NotChanged
&& attachmentsUpdated == AttachmentsUpdateResult.NotChanged
) return;

updatedCount.incrementAndGet()

when (pageResult) {
is PageOperationResult.Created -> {
printWithPrefix("${green("Created:")} ${pageInfo(pageResult.serverPage, pageResult.local)}")
}

is PageOperationResult.ContentModified,
is PageOperationResult.LocationModified-> {
describeModifiedPage("Updated:", pageResult.serverPage, pageResult.local, labelUpdate, attachmentsUpdated)
}

is PageOperationResult.NotModified -> {
if (labelUpdate != LabelsUpdateResult.NotChanged || attachmentsUpdated != AttachmentsUpdateResult.NotChanged) {
describeModifiedPage("Updated labels/attachments:", pageResult.serverPage, pageResult.local, labelUpdate, attachmentsUpdated)
}
}
}
}

private fun describeModifiedPage(
operation: String,
serverPage: ServerPage,
local: Page,
labelUpdate: LabelsUpdateResult,
attachmentsUpdated: AttachmentsUpdateResult
) {
val labelsAttachmentsInfo = labelsAttachmentsInfo(labelUpdate, attachmentsUpdated)
printWithPrefix(
"${cyan(operation)} ${
pageInfo(
serverPage,
local
)
}${if (labelsAttachmentsInfo.isNotBlank()) " $labelsAttachmentsInfo" else "" }"
)
}

private fun pageInfo(serverPage: ServerPage, page: Page): String = buildString {
append('"')
append(blue(serverPage.title))
append('"')
append(" from - ")
append(page.source.normalize())
append(".")
val uiLink = serverPage.links[UILINK]
if (uiLink != null) {
append(" URL - ")
append(URLBuilder(server).appendPathSegments(uiLink).buildString())
append(".")
}
}

private fun labelsAttachmentsInfo(labelsUpdateResult: LabelsUpdateResult, attachmentsUpdated: AttachmentsUpdateResult): String {
val labelsInfo = buildString {
if (labelsUpdateResult is LabelsUpdateResult.Updated) {
append("Labels ")
if (labelsUpdateResult.added.isNotEmpty()) {
append(green("+"))
append("[")
append(labelsUpdateResult.added.joinToString(", "))
append("]")
if (labelsUpdateResult.removed.isNotEmpty()) {
append(", ")
}
}
if (labelsUpdateResult.removed.isNotEmpty()) {
append(red("-"))
append("[")
append(labelsUpdateResult.removed.joinToString(", "))
append("]")
}
}
}
val attachmentsInfo = buildString {
if (attachmentsUpdated is AttachmentsUpdateResult.Updated) {
append("attachments: ")
append(green("added ${attachmentsUpdated.added.size}, "))
append(cyan("modified ${attachmentsUpdated.modified.size}, "))
append(red("removed ${attachmentsUpdated.removed.size}"))
}
}
val labelsAttachmentsDetails = listOf(labelsInfo, attachmentsInfo).filter { it.isNotBlank() }
return if (labelsAttachmentsDetails.isEmpty()) {
return ""
} else {
labelsAttachmentsDetails.joinToString(", ", postfix = ".")
}
}

override fun uploadsCompleted() {
val updated = updatedCount.get()
if (updated == 0L) {
printer(green("All pages are up to date"))
}
}

override fun pagesDeleted(root: ConfluencePage, allDeletedPages: List<ConfluencePage>) {
if (allDeletedPages.isEmpty()) return

printWithPrefix(buildString {
append(red("Deleted:"))
append(" ")
append(deletedPage(allDeletedPages[0]))
if (allDeletedPages.size > 1) {
append(" with subpages:")
}
})

val tail = allDeletedPages.drop(1)
if (tail.isNotEmpty()) {
tail.forEach { page ->
printWithPrefix("${red(" Deleted:")} ${deletedPage(page)}")
}
}
}

private fun deletedPage(confluencePage: ConfluencePage): String {
return buildString {
append("\"")
append(blue(confluencePage.title))
append("\"")
append(" (")
append(confluencePage.id)
append(")")
}
}

private fun printWithPrefix(msg: String) {
printer("$prefix$msg")
}
}
12 changes: 11 additions & 1 deletion cli/src/main/kotlin/com/github/zeldigas/text2confl/cli/Upload.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.github.zeldigas.text2confl.cli

import PrintingUploadOperationsTracker
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.PrintMessage
import com.github.ajalt.clikt.core.requireObject
import com.github.ajalt.clikt.core.terminal
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.types.enum
Expand All @@ -13,6 +15,7 @@ import com.github.zeldigas.text2confl.convert.EditorVersion
import com.github.zeldigas.text2confl.core.ServiceProvider
import com.github.zeldigas.text2confl.core.config.*
import com.github.zeldigas.text2confl.core.upload.ChangeDetector
import com.github.zeldigas.text2confl.core.upload.UploadOperationTracker
import io.ktor.client.plugins.logging.*
import io.ktor.http.*
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -66,6 +69,7 @@ class Upload : CliktCommand(name = "upload", help = "Converts source files and u

override fun run() = runBlocking {
try {
configureRequestLoggingIfEnabled()
tryUpload()
} catch (ex: Exception) {
tryHandleException(ex)
Expand All @@ -87,12 +91,18 @@ class Upload : CliktCommand(name = "upload", help = "Converts source files and u
val confluenceClient = serviceProvider.createConfluenceClient(clientConfig, dryRun)
val publishUnder = resolveParent(confluenceClient, uploadConfig, directoryStoredParams)

val contentUploader = serviceProvider.createUploader(confluenceClient, uploadConfig, conversionConfig)
val contentUploader = serviceProvider.createUploader(confluenceClient, uploadConfig, conversionConfig, operationsTracker(clientConfig.server))
withContext(Dispatchers.Default) {
contentUploader.uploadPages(pages = result, uploadConfig.space, publishUnder)
}
}

private fun operationsTracker(server: Url): UploadOperationTracker = PrintingUploadOperationsTracker(
server = server,
printer = terminal::println,
prefix = if (dryRun) "(dryrun) " else ""
)

private fun createUploadConfig(configuration: DirectoryConfig): UploadConfig {
val orphanRemoval = if (docs.isFile) {
Cleanup.None
Expand Down
Loading

0 comments on commit 708e73d

Please sign in to comment.