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

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