All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.clapper.avsl.formatter.simple.scala Maven / Gradle / Ivy

The newest version!
/*
  ---------------------------------------------------------------------------
  This software is released under a BSD license, adapted from
  http://opensource.org/licenses/bsd-license.php

  Copyright (c) 2010, Brian M. Clapper
  All rights reserved.

  Redistribution and use in source and binary forms, with or without
  modification, are permitted provided that the following conditions are
  met:

    * Redistributions of source code must retain the above copyright notice,
    this list of conditions and the following disclaimer.

    * Redistributions in binary form must reproduce the above copyright
    notice, this list of conditions and the following disclaimer in the
    documentation and/or other materials provided with the distribution.

    * Neither the names "clapper.org", "AVSL", nor the names of its
    contributors may be used to endorse or promote products derived from
    this software without specific prior written permission.

  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
  IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
  THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
  PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  ---------------------------------------------------------------------------
*/

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-23)
  * - %H: the hour of the day (1-12)
  * - %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") match {
    case None         => TimeZone.getDefault
    case Some(tzName) => TimeZone.getTimeZone(tzName)
  }


  // 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 = {
    logMessage.exception match {
      case None =>
        dateFormat.format(logMessage)

      case Some(t) => {
        import java.io.{PrintWriter, StringWriter}

        val sw = new StringWriter
        t.printStackTrace(new PrintWriter(sw))
        dateFormat.format(logMessage) + " " + sw.toString
      }
    }
  }
}

/**
  * 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