![JAR search and dependency download from the Maven repository](/logo.png)
org.clapper.avsl.formatter.simple.scala Maven / Gradle / Ivy
The newest version!
package org.clapper.avsl.formatter
import org.clapper.avsl.{LogLevel, LogMessage}
import org.clapper.avsl.config.ConfiguredArguments
import java.util.{Calendar, Date, Locale, TimeZone}
import java.text.{DateFormat, SimpleDateFormat}
/**
* `SimpleFormatter` represents the default formatter for the AVSL
* logger. It uses simple %-escaped format strings, akin to the standard
* C `strftime()` function. These escapes, described below, are more compact
* than the format strings used by Java's `SimpleDateFormat` class; they
* also don't suffer from the odd quoting conventions imposed by
* `SimpleDateFormat`. However, they are mapped to `SimpleDateFormat`
* patterns, so they are locale-, language-, and time zone-sensitive.
*
* A `SimpleFormatter` accepts the following name/value pair arguments:
*
* - `format`: The format to use. If not specified, there's a reasonable
* default
* - `language`: The language to use when formatting dates, using the Java
* `Locale` values. If not specified, the default locale is used.
* - `country`: The country to use when formatting dates, using the Java
* `Locale` values. If not specified, the default locale is used.
* - `tz`: The time zone to use. If not specified, the default is used.
*
* The recognized format escapes are shown below. Anything else is displayed
* literally. Many of the escapes are borrowed directly from `strftime()`.
*
* - %a: the short day-of-week name (e.g., "Wed")
* - %A: the long day-of-week name (e.g., "Wednesday")
* - %b: the abbreviated month name (e.g., "Mar", "Nov")
* - %B: the full month name (e.g., "March", "November")
* - %d: the day of the month
* - %D: equivalent to %m/%d/%y
* - %F: equivalent to %Y/%m/%d
* - %h: the hour of the day (0-12)
* - %H: the hour of the day (1-23)
* - %j: the day of the year (i.e., the so-called Julian day)
* - %l: the log level name (e.g., "INFO", "DEBUG")
* - %L: the log level's numeric value
* - %m: the month number (01-12)
* - %M: the current minute, zero-padded
* - %n: the short name of the logger (i.e., the last part of the class name)
* - %N: the full name of the logger (i.e., the class name)
* - %s: the current second, zero-padded
* - %S: the current millisecond, zero-padded
* - %t: the text of the log message
* - %T: the current thread name
* - %y: the 2-digit year
* - %Y: the full 4-digit year
* - %z: the time zone name (e.g., "UTC", "PDT", "EST")
* - %%: a literal "%"
*/
class SimpleFormatter(args: ConfiguredArguments) extends Formatter {
import java.text.SimpleDateFormat
val DefaultFormat = "[%Y/%m/%d %H:%M:%s:%S] %l %n %t"
val formatString = args.getOrElse("format", DefaultFormat)
val defaultLocale = Locale.getDefault
val language = args.getOrElse("language", defaultLocale.getLanguage)
val country = args.getOrElse("country", defaultLocale.getCountry)
val tz = args.get("tz").
map { tzName => TimeZone.getTimeZone(tzName) }.
getOrElse(TimeZone.getDefault)
// Must be lazy, to ensure that they is evaluated after the variables,
// above, are initialized.
lazy val locale = new Locale(language, country)
private lazy val dateFormat = new ParsedPattern(formatString, locale, tz)
def format(logMessage: LogMessage): String = {
def mapThrowable(t: Throwable) = {
import java.io.{PrintWriter, StringWriter}
val sw = new StringWriter
t.printStackTrace(new PrintWriter(sw))
dateFormat.format(logMessage) + " " + sw.toString
}
logMessage.exception.map { mapThrowable(_) }.
getOrElse(dateFormat.format(logMessage))
}
}
/**
* A parsed format. In the parsed format, the format is broken into tokens,
* each of which is associated with a function. Formatting a message means
* means passing the message to all the functions and then concatenating
* the result. This approach avoids odd escaping problems with Java's
* `SimpleDateFormat` strings, because each token is separately handled,
* and literals are extracted and processed independently of format
* strings. Storing the functions, rather than the strings, means we can
* curry the functions and create the `SimpleDateFormat` objects ahead of
* time.
*/
private class ParsedPattern(originalPattern: String,
locale: Locale,
tz: TimeZone) {
val parsedPattern: List[(LogMessage) => String] =
parse(originalPattern.toList)
private lazy val Mappings = Map[Char, LogMessage => String](
'a' -> datePatternFunc("E"),
'A' -> datePatternFunc("EEEE"),
'b' -> datePatternFunc("MMM"),
'B' -> datePatternFunc("MMMM"),
'd' -> datePatternFunc("dd"),
'D' -> datePatternFunc("MM/dd/yy"),
'F' -> datePatternFunc("yyyy-MM-dd"),
'h' -> datePatternFunc("hh"),
'H' -> datePatternFunc("HH"),
'j' -> datePatternFunc("D"),
'l' -> insertLevelName _,
'L' -> insertLevelValue _,
'M' -> datePatternFunc("mm"),
'm' -> datePatternFunc("MM"),
'n' -> insertName(true) _,
'N' -> insertName(false) _,
's' -> datePatternFunc("ss"),
'S' -> datePatternFunc("SSS"),
't' -> insertMessage _,
'T' -> insertThreadName _,
'y' -> datePatternFunc("yy"),
'Y' -> datePatternFunc("yyyy"),
'z' -> datePatternFunc("z"),
'%' -> copyLiteralFunc("%")
)
/** Format a log message, using the parsed pattern.
*
* @param logMessage the message
*
* @return the formatted string
*/
def format(logMessage: LogMessage): String =
parsedPattern.map(_(logMessage)).mkString("")
override def toString = originalPattern
private def insertThreadName(msg: LogMessage): String =
Thread.currentThread.getName
private def insertLevelValue(msg: LogMessage): String =
msg.level.value.toString
private def insertLevelName(msg: LogMessage): String = msg.level.label
private def insertMessage(msg: LogMessage): String = msg.message.toString
private def insertName(short: Boolean)(msg: LogMessage): String =
if (short) msg.name.split("""\.""").last else msg.name
private def insertDateChunk(format: DateFormat)(msg: LogMessage): String = {
val cal = Calendar.getInstance(tz, locale)
cal.setTimeInMillis(msg.date)
format.format(cal.getTime)
}
private def datePatternFunc(pattern: String) =
insertDateChunk(new SimpleDateFormat(pattern, locale)) _
private def copyLiteral(s: String)(msg: LogMessage): String = s
private def copyLiteralFunc(s: String) = copyLiteral(s) _
private def escape(ch: Char): List[LogMessage => String] =
List(Mappings.getOrElse(ch, copyLiteralFunc("'%" + ch + "'")))
private def parse(stream: List[Char], gathered: String = ""):
List[LogMessage => String] = {
def gatheredFuncList = {
if (gathered == "") Nil else List(copyLiteralFunc(gathered))
}
stream match {
case Nil if (gathered != "") =>
List(copyLiteralFunc(gathered))
case Nil =>
Nil
case '%' :: Nil =>
gatheredFuncList ::: List(copyLiteralFunc("%"))
case '%' :: tail =>
gatheredFuncList ::: escape(tail(0)) ::: parse(tail drop 1)
case c :: tail =>
parse(tail, gathered + c)
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy