kadai.log.log4j.JsonLayout.scala Maven / Gradle / Ivy
The newest version!
package kadai
package log
package log4j
import collection.JavaConverters.{ mapAsJavaMapConverter, mapAsScalaMapConverter }
import io.circe.{ Decoder, Encoder, Json, Printer }
import java.nio.charset.Charset
import org.apache.logging.log4j.Level
import org.apache.logging.log4j.core.{ LogEvent => CoreLogEvent }
import org.apache.logging.log4j.core.config.plugins.{PluginAttribute, Plugin, PluginFactory}
import org.apache.logging.log4j.core.layout.AbstractStringLayout
import org.joda.time.DateTime
import kadai.log.json.JsonMessage
/**
* JsonLayout is an alternate layout that outputs each line as a JSON object.
* It does not attempt to produce a file that contains valid JSON however,
* and would need additional processing to turn this series of distinct objects
* into a well formed JSON array. It is specifically designed for processing
* each log message as a separate JSON object.
*
* To use it, you simply need to log `JsonMessage` objects, which you can easily
* produce by mixing in the `JsonLogging` trait, importing `JsonLogging._` and
* having an `argonaut.EncodeJson[Foo]` in scope for the Foo object you want to
* log. This EncodeJson instance is then used to control the format of the log
* output.
*
* If you do not have an EncodeJson for the message object, then it will fall back
* to a String representation given by the Show[Foo] instance in scope.
*
* Usage:
* {{{
* import argonaut._, Argonaut._
*
* trait Foo { def bar: Bar }
* object Foo extends JsonLogging {
* import JsonLogging._
*
* def foozle(foo: Foo) =
* info(foo)
*
* implicit val FooEncoder: EncodeJson[Foo] =
* // assuming there is already an EncodeJson[Bar]
* EncodeJson { foo =>
* ("foobar" := foo.bar) ->: Json.jEmptyObject
* }
* // results in {"foo":{"bar":"..."}}
* }
* }}}
*
* see http://argonaut.io/doc/codec/ for more details on how to build an EncodeJson
*/
object JsonLayout {
@PluginFactory
def createLayout(
@PluginAttribute(value = "printer", defaultString = "nospace") printer: String,
@PluginAttribute(value = "charset", defaultString = "UTF8") charset: String
): JsonLayout =
new JsonLayout(printer, charset)
object Printers {
val Nospace = "nospace"
val Spaces2 = "spaces2"
val Spaces4 = "spaces4"
def apply(s: String): Printer =
s match {
case Nospace => Printer.noSpaces
case Spaces2 => Printer.spaces2
case Spaces4 => Printer.spaces4
case _ => Printer.noSpaces
}
}
}
@Plugin(name = "Json", category = "Core", elementType = "layout", printObject = true)
final class JsonLayout(p: String, charset: String) extends AbstractStringLayout(Charset.forName(charset)) {
import Event._
import JsonLayout.Printers._
private val newLine =
System.getProperty("line.separator")
private val printer: Printer =
JsonLayout.Printers {
Option(p).orElse { Option(System.getProperty("kadai.log.json.printer")) }.getOrElse(Nospace)
}
override def getContentType =
"application/json"
override def getContentFormat(): java.util.Map[String, String] =
Map[String, String]().asJava
override def toSerializable(ev: CoreLogEvent): String =
printer.pretty {
Encoder[Event].apply {
val contextMap = ev.getContextMap
Event(
new DateTime(ev.getTimeMillis),
ev.getLoggerName,
ev.getLevel,
if (contextMap.isEmpty) None else Some(contextMap.asScala.toMap),
ev.getMessage match {
// json
case Log4jMessage(m@JsonMessage(_, json)) => json
case m => JsonMessage.Qualified[String].fields(m.getFormattedMessage)
},
Option(ev.getThreadName),
Option(ev.getThrown)
)
}
} + newLine
}
case class Event(
time: DateTime,
logger: String,
lvl: Level,
ctx: Option[Map[String, String]],
json: Seq[(String, Json)],
threadName: Option[String],
thrown: Option[Throwable])
object Event {
import kadai.log.json.JsonLogging._
val DateFormat =
org.joda.time.format.DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ").withZoneUTC
implicit val EncodeJsonDateTime: Encoder[DateTime] =
Encoder[String].contramap { DateFormat.print }
implicit val DecodeDateTime: Decoder[DateTime] =
Decoder[String].map { DateFormat.parseDateTime }
implicit val LevelEncoder: Encoder[Level] =
Encoder[String].contramap { _.name }
implicit val LevelDecoder: Decoder[Level] =
Decoder[String].map { Level.toLevel }
implicit val EventEncoder: Encoder[Event] =
Encoder.instance {
case Event(t, logger, lvl, ctx, json, threadName, thrown) =>
import io.circe.syntax._
Json.fromFields(
("time" -> t.asJson) +:
("logger" -> logger.asJson) +:
("level" -> lvl.asJson) +:
("ctx" -> ctx.asJson) +:
("thread" -> threadName.asJson) +:
("thrown" -> thrown.asJson) +:
json
)
}
implicit val EventDecoder: Decoder[Event] =
Decoder.instance { cursor =>
for {
tim <- cursor.get[DateTime]("time").right
log <- cursor.get[String]("logger").right
lvl <- cursor.get[Level]("level").right
ctx <- cursor.get[Option[Map[String, String]]]("ctx").right
threadName <- cursor.get[Option[String]]("thread").right
json <- Right(cursor.focus.flatMap(_.asObject).map[Seq[(String, Json)]](_.toList.filterNot { case (name, _) => List("time", "logger", "level", "ctx", "thread", "thrown").contains(name) })).right
} yield Event(tim, log, lvl, ctx, json.get, threadName, None)
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy