PatternFormat.kt

package com.hexagontk.jul

import com.hexagontk.core.text.AnsiColor.DEFAULT
import com.hexagontk.core.text.AnsiColor.YELLOW
import com.hexagontk.core.text.AnsiColor.BLUE
import com.hexagontk.core.text.AnsiColor.BRIGHT_BLACK
import com.hexagontk.core.text.AnsiColor.CYAN
import com.hexagontk.core.text.AnsiColor.MAGENTA
import com.hexagontk.core.text.AnsiColor.RED
import com.hexagontk.core.text.AnsiEffect.BOLD
import com.hexagontk.core.text.Ansi.RESET
import com.hexagontk.core.text.eol
import com.hexagontk.core.fail
import com.hexagontk.core.toText
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import java.util.logging.Formatter
import java.util.logging.Level
import java.util.logging.LogRecord

/**
 * A Formatter implements [Formatter] provides support for formatting Logs.
 *
 * @property useColor Use colors in log messages.
 */
class PatternFormat(
    private val useColor: Boolean,
    private val messageOnly: Boolean = false,
) : Formatter() {

    internal companion object {
        private const val TIMESTAMP = "%tH:%<tM:%<tS,%<tL"
        private const val LEVEL = "%-5s"
        private const val THREAD = "[%-15s]"
        private const val LOGGER = "%-30s"

        const val PATTERN = "$TIMESTAMP $LEVEL $THREAD $LOGGER | %s%n"
        const val COLOR_PATTERN =
            "$BRIGHT_BLACK$TIMESTAMP %s$LEVEL$RESET $MAGENTA$THREAD $CYAN$LOGGER$RESET | %s%n$RESET"
    }

    private val pattern: String = if (useColor) COLOR_PATTERN else PATTERN

    private val levelColors: Map<Level, String> = mapOf(
        Level.FINER to DEFAULT,
        Level.FINE to DEFAULT,
        Level.INFO to BLUE,
        Level.WARNING to YELLOW,
        Level.SEVERE to RED + BOLD
    )

    private val levelNames: Map<Level, String> = mapOf(
        Level.FINER to "TRACE",
        Level.FINE to "DEBUG",
        Level.INFO to "INFO",
        Level.WARNING to "WARN",
        Level.SEVERE to "ERROR"
    )

    override fun format(record: LogRecord): String {
        if (messageOnly)
            return record.message + "\n"

        val instant = Instant.ofEpochMilli(record.millis)
        val dateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault())
        val thrown = record.thrown
        val trace = when {
            thrown == null -> ""
            useColor -> "$eol${thrown.toText()}".replace("\n", "\n$RED")
            else -> "$eol${thrown.toText()}"
        }
        val level = record.level
        val levelName = levelNames[level] ?: fail
        val levelColor = levelColors[level] ?: BLUE
        val message = record.message
        val loggerName = record.loggerName
        val threadName = Thread.currentThread().name

        return if (useColor)
            pattern.format(dateTime, levelColor, levelName, threadName, loggerName, message + trace)
        else
            pattern.format(dateTime, levelName, threadName, loggerName, message + trace)
    }
}