diff --git a/build.sbt b/build.sbt index becdb12cf..86898c559 100644 --- a/build.sbt +++ b/build.sbt @@ -99,12 +99,12 @@ lazy val utilComplete = (project in internalPath / "util-complete"). // logging lazy val utilLogging = (project in internalPath / "util-logging"). enablePlugins(ContrabandPlugin, JsonCodecPlugin). - dependsOn(utilInterface, utilTesting % Test). + dependsOn(utilInterface, utilCollection, utilTesting % Test). settings( commonSettings, crossScalaVersions := Seq(scala210, scala211, scala212), name := "Util Logging", - libraryDependencies ++= Seq(jline, log4jApi, log4jCore, disruptor, sjsonnewScalaJson), + libraryDependencies ++= Seq(jline, log4jApi, log4jCore, disruptor, sjsonnewScalaJson, scalaReflect.value), sourceManaged in (Compile, generateContrabands) := baseDirectory.value / "src" / "main" / "contraband-scala" ) diff --git a/internal/util-collection/src/main/scala/sbt/internal/util/INode.scala b/internal/util-collection/src/main/scala/sbt/internal/util/INode.scala index d85cadf3f..3c159ec11 100644 --- a/internal/util-collection/src/main/scala/sbt/internal/util/INode.scala +++ b/internal/util-collection/src/main/scala/sbt/internal/util/INode.scala @@ -95,7 +95,7 @@ abstract class EvaluateSettings[Scope] { keyString private[this] def keyString = - (static.toSeq.flatMap { case (key, value) => if (value eq this) init.showFullKey(key) :: Nil else Nil }).headOption getOrElse "non-static" + (static.toSeq.flatMap { case (key, value) => if (value eq this) init.showFullKey.show(key) :: Nil else Nil }).headOption getOrElse "non-static" final def get: T = synchronized { assert(value != null, toString + " not evaluated") diff --git a/internal/util-collection/src/main/scala/sbt/internal/util/Settings.scala b/internal/util-collection/src/main/scala/sbt/internal/util/Settings.scala index 97117e2dc..afa02c150 100644 --- a/internal/util-collection/src/main/scala/sbt/internal/util/Settings.scala +++ b/internal/util-collection/src/main/scala/sbt/internal/util/Settings.scala @@ -6,6 +6,7 @@ package sbt.internal.util import scala.language.existentials import Types._ +import sbt.util.Show sealed trait Settings[Scope] { def data: Map[Scope, AttributeMap] @@ -119,7 +120,7 @@ trait Init[Scope] { def mapScope(f: Scope => Scope): MapScoped = new MapScoped { def apply[T](k: ScopedKey[T]): ScopedKey[T] = k.copy(scope = f(k.scope)) } - private final class InvalidReference(val key: ScopedKey[_]) extends RuntimeException("Internal settings error: invalid reference to " + showFullKey(key)) + private final class InvalidReference(val key: ScopedKey[_]) extends RuntimeException("Internal settings error: invalid reference to " + showFullKey.show(key)) private[this] def applyDefaults(ss: Seq[Setting[_]]): Seq[Setting[_]] = { @@ -215,11 +216,11 @@ trait Init[Scope] { { val guessed = guessIntendedScope(validKeys, delegates, u.referencedKey) val derived = u.defining.isDerived - val refString = display(u.defining.key) + val refString = display.show(u.defining.key) val sourceString = if (derived) "" else parenPosString(u.defining) - val guessedString = if (derived) "" else guessed.map(g => "\n Did you mean " + display(g) + " ?").toList.mkString + val guessedString = if (derived) "" else guessed.map(g => "\n Did you mean " + display.show(g) + " ?").toList.mkString val derivedString = if (derived) ", which is a derived setting that needs this key to be defined in this scope." else "" - display(u.referencedKey) + " from " + refString + sourceString + derivedString + guessedString + display.show(u.referencedKey) + " from " + refString + sourceString + derivedString + guessedString } private[this] def parenPosString(s: Setting[_]): String = s.positionString match { case None => ""; case Some(s) => " (" + s + ")" } @@ -255,7 +256,7 @@ trait Init[Scope] { new Uninitialized(keys, prefix + suffix + " to undefined setting" + suffix + ": " + keysString + "\n ") } final class Compiled[T](val key: ScopedKey[T], val dependencies: Iterable[ScopedKey[_]], val settings: Seq[Setting[T]]) { - override def toString = showFullKey(key) + override def toString = showFullKey.show(key) } final class Flattened(val key: ScopedKey[_], val dependencies: Iterable[ScopedKey[_]]) diff --git a/internal/util-collection/src/main/scala/sbt/internal/util/Show.scala b/internal/util-collection/src/main/scala/sbt/internal/util/Show.scala deleted file mode 100644 index 4a0343ed7..000000000 --- a/internal/util-collection/src/main/scala/sbt/internal/util/Show.scala +++ /dev/null @@ -1,8 +0,0 @@ -package sbt.internal.util - -trait Show[T] { - def apply(t: T): String -} -object Show { - def apply[T](f: T => String): Show[T] = new Show[T] { def apply(t: T): String = f(t) } -} \ No newline at end of file diff --git a/internal/util-collection/src/main/scala/sbt/util/Show.scala b/internal/util-collection/src/main/scala/sbt/util/Show.scala new file mode 100644 index 000000000..20ac0565d --- /dev/null +++ b/internal/util-collection/src/main/scala/sbt/util/Show.scala @@ -0,0 +1,12 @@ +package sbt.util + +trait Show[A] { + def show(a: A): String +} +object Show { + def apply[A](f: A => String): Show[A] = new Show[A] { def show(a: A): String = f(a) } + + def fromToString[A]: Show[A] = new Show[A] { + def show(a: A): String = a.toString + } +} diff --git a/internal/util-collection/src/main/scala/sbt/internal/util/ShowLines.scala b/internal/util-collection/src/main/scala/sbt/util/ShowLines.scala similarity index 92% rename from internal/util-collection/src/main/scala/sbt/internal/util/ShowLines.scala rename to internal/util-collection/src/main/scala/sbt/util/ShowLines.scala index f99a1394c..65729d747 100644 --- a/internal/util-collection/src/main/scala/sbt/internal/util/ShowLines.scala +++ b/internal/util-collection/src/main/scala/sbt/util/ShowLines.scala @@ -1,4 +1,4 @@ -package sbt.internal.util +package sbt.util trait ShowLines[A] { def showLines(a: A): Seq[String] diff --git a/internal/util-collection/src/test/scala/SettingsExample.scala b/internal/util-collection/src/test/scala/SettingsExample.scala index cf65d6c68..20536a5ef 100644 --- a/internal/util-collection/src/test/scala/SettingsExample.scala +++ b/internal/util-collection/src/test/scala/SettingsExample.scala @@ -1,5 +1,7 @@ package sbt.internal.util +import sbt.util.Show + /** Define our settings system */ // A basic scope indexed by an integer. @@ -12,9 +14,10 @@ final case class Scope(nestIndex: Int, idAtIndex: Int = 0) // That would be a general pain.) case class SettingsExample() extends Init[Scope] { // Provides a way of showing a Scope+AttributeKey[_] - val showFullKey: Show[ScopedKey[_]] = new Show[ScopedKey[_]] { - def apply(key: ScopedKey[_]) = s"${key.scope.nestIndex}(${key.scope.idAtIndex})/${key.key.label}" - } + val showFullKey: Show[ScopedKey[_]] = Show[ScopedKey[_]]((key: ScopedKey[_]) => + { + s"${key.scope.nestIndex}(${key.scope.idAtIndex})/${key.key.label}" + }) // A sample delegation function that delegates to a Scope with a lower index. val delegates: Scope => Seq[Scope] = { diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleAppender.scala b/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleAppender.scala index 540f84436..98f404d34 100644 --- a/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleAppender.scala +++ b/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleAppender.scala @@ -5,11 +5,10 @@ import java.io.{ PrintStream, PrintWriter } import java.util.Locale import java.util.concurrent.atomic.AtomicInteger import org.apache.logging.log4j.{ Level => XLevel } -import org.apache.logging.log4j.message.{ Message, ParameterizedMessage, ObjectMessage, ReusableObjectMessage } +import org.apache.logging.log4j.message.{ Message, ObjectMessage, ReusableObjectMessage } import org.apache.logging.log4j.core.{ LogEvent => XLogEvent } import org.apache.logging.log4j.core.appender.AbstractAppender import org.apache.logging.log4j.core.layout.PatternLayout -import org.apache.logging.log4j.core.async.RingBufferLogEvent import ConsoleAppender._ @@ -246,25 +245,30 @@ class ConsoleAppender private[ConsoleAppender] ( { val level = ConsoleAppender.toLevel(event.getLevel) val message = event.getMessage - val str = messageToString(message) - appendLog(level, str) + // val str = messageToString(message) + appendMessage(level, message) } - def messageToString(msg: Message): String = + def appendMessage(level: Level.Value, msg: Message): Unit = msg match { - case p: ParameterizedMessage => p.getFormattedMessage - case r: RingBufferLogEvent => r.getFormattedMessage - case o: ObjectMessage => objectToString(o.getParameter) - case o: ReusableObjectMessage => objectToString(o.getParameter) - case _ => msg.getFormattedMessage + case o: ObjectMessage => objectToLines(o.getParameter) foreach { appendLog(level, _) } + case o: ReusableObjectMessage => objectToLines(o.getParameter) foreach { appendLog(level, _) } + case _ => appendLog(level, msg.getFormattedMessage) } - def objectToString(o: AnyRef): String = + def objectToLines(o: AnyRef): Vector[String] = o match { - case x: StringEvent => x.message - case x: ObjectEvent[_] => x.message.toString - case _ => o.toString + case x: StringEvent => Vector(x.message) + case x: ObjectEvent[_] => objectEventToLines(x) + case _ => Vector(o.toString) + } + def objectEventToLines(oe: ObjectEvent[_]): Vector[String] = + { + val tag = oe.tag + LogExchange.stringCodec[AnyRef](tag) match { + case Some(codec) => codec.showLines(oe.message.asInstanceOf[AnyRef]).toVector + case _ => Vector(oe.message.toString) + } } - def messageColor(level: Level.Value) = RESET def labelColor(level: Level.Value) = level match { diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/ManagedLogger.scala b/internal/util-logging/src/main/scala/sbt/internal/util/ManagedLogger.scala index 1564b224e..a7b84990a 100644 --- a/internal/util-logging/src/main/scala/sbt/internal/util/ManagedLogger.scala +++ b/internal/util-logging/src/main/scala/sbt/internal/util/ManagedLogger.scala @@ -4,6 +4,7 @@ import sbt.util._ import org.apache.logging.log4j.{ Logger => XLogger } import org.apache.logging.log4j.message.ObjectMessage import sjsonnew.JsonFormat +import scala.reflect.runtime.universe.TypeTag /** * Delegates log events to the associated LogExchange. @@ -24,16 +25,24 @@ class ManagedLogger( } override def success(message: => String): Unit = xlogger.info(message) - final def debugEvent[A: JsonFormat](event: => A): Unit = logEvent(Level.Debug, event) - final def infoEvent[A: JsonFormat](event: => A): Unit = logEvent(Level.Info, event) - final def warnEvent[A: JsonFormat](event: => A): Unit = logEvent(Level.Warn, event) - final def errorEvent[A: JsonFormat](event: => A): Unit = logEvent(Level.Error, event) - def logEvent[A: JsonFormat](level: Level.Value, event: => A): Unit = + def registerStringCodec[A: ShowLines: TypeTag]: Unit = + { + val tag = StringTypeTag[A] + val ev = implicitly[ShowLines[A]] + // println(s"registerStringCodec ${tag.key}") + val _ = LogExchange.getOrElseUpdateStringCodec(tag.key, ev) + } + final def debugEvent[A: JsonFormat: TypeTag](event: => A): Unit = logEvent(Level.Debug, event) + final def infoEvent[A: JsonFormat: TypeTag](event: => A): Unit = logEvent(Level.Info, event) + final def warnEvent[A: JsonFormat: TypeTag](event: => A): Unit = logEvent(Level.Warn, event) + final def errorEvent[A: JsonFormat: TypeTag](event: => A): Unit = logEvent(Level.Error, event) + def logEvent[A: JsonFormat: TypeTag](level: Level.Value, event: => A): Unit = { val v: A = event - val clazz: Class[A] = v.getClass.asInstanceOf[Class[A]] - val ev = LogExchange.getOrElseUpdateJsonCodec(clazz, implicitly[JsonFormat[A]]) - val entry: ObjectEvent[A] = new ObjectEvent(level, v, channelName, execId, ev, clazz) + val tag = StringTypeTag[A] + LogExchange.getOrElseUpdateJsonCodec(tag.key, implicitly[JsonFormat[A]]) + // println("logEvent " + tag.key) + val entry: ObjectEvent[A] = new ObjectEvent(level, v, channelName, execId, tag.key) xlogger.log( ConsoleAppender.toXLevel(level), new ObjectMessage(entry) diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/ObjectEvent.scala b/internal/util-logging/src/main/scala/sbt/internal/util/ObjectEvent.scala index 611af321b..13cddfb83 100644 --- a/internal/util-logging/src/main/scala/sbt/internal/util/ObjectEvent.scala +++ b/internal/util-logging/src/main/scala/sbt/internal/util/ObjectEvent.scala @@ -10,8 +10,6 @@ final class ObjectEvent[A]( val message: A, val channelName: Option[String], val execId: Option[String], - val ev: JsonFormat[A], - val clazz: Class[A] + val tag: String ) extends Serializable { - } diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/StringTypeTag.scala b/internal/util-logging/src/main/scala/sbt/internal/util/StringTypeTag.scala new file mode 100644 index 000000000..20d226884 --- /dev/null +++ b/internal/util-logging/src/main/scala/sbt/internal/util/StringTypeTag.scala @@ -0,0 +1,29 @@ +package sbt.internal.util + +import scala.reflect.runtime.universe._ + +/** This is used to carry type information in JSON. */ +final case class StringTypeTag[A](key: String) { + override def toString: String = key +} + +object StringTypeTag { + def apply[A: TypeTag]: StringTypeTag[A] = + { + val tag = implicitly[TypeTag[A]] + val tpe = tag.tpe + val k = typeToString(tpe) + // println(tpe.getClass.toString + " " + k) + StringTypeTag[A](k) + } + def typeToString(tpe: Type): String = + tpe match { + case ref: TypeRef => + if (ref.args.nonEmpty) { + val typeCon = ref.typeConstructor.typeSymbol.asType.fullName + val typeArgs = ref.typeArgs map typeToString + s"""$typeCon[${typeArgs.mkString(",")}]""" + } else tpe.toString + case _ => tpe.toString + } +} diff --git a/internal/util-logging/src/main/scala/sbt/util/LogExchange.scala b/internal/util-logging/src/main/scala/sbt/util/LogExchange.scala index 8914b4998..1dc17804d 100644 --- a/internal/util-logging/src/main/scala/sbt/util/LogExchange.scala +++ b/internal/util-logging/src/main/scala/sbt/util/LogExchange.scala @@ -15,7 +15,8 @@ import sjsonnew.JsonFormat sealed abstract class LogExchange { private[sbt] lazy val context: LoggerContext = init() private[sbt] lazy val asyncStdout: AsyncAppender = buildAsyncStdout - private[sbt] val jsonCodecs: concurrent.Map[Class[_], JsonFormat[_]] = concurrent.TrieMap() + private[sbt] val jsonCodecs: concurrent.Map[String, JsonFormat[_]] = concurrent.TrieMap() + private[sbt] val stringCodecs: concurrent.Map[String, ShowLines[_]] = concurrent.TrieMap() def logger(name: String): ManagedLogger = logger(name, None, None) def logger(name: String, channelName: Option[String], execId: Option[String]): ManagedLogger = { @@ -46,12 +47,18 @@ sealed abstract class LogExchange { val config = ctx.getConfiguration config.getLoggerConfig(loggerName) } - def jsonCodec[A](clazz: Class[A]): Option[JsonFormat[A]] = - jsonCodecs.get(clazz) map { _.asInstanceOf[JsonFormat[A]] } - def hasJsonCodec[A](clazz: Class[A]): Boolean = - jsonCodecs.contains(clazz) - def getOrElseUpdateJsonCodec[A](clazz: Class[A], v: JsonFormat[A]): JsonFormat[A] = - jsonCodecs.getOrElseUpdate(clazz, v).asInstanceOf[JsonFormat[A]] + def jsonCodec[A](tag: String): Option[JsonFormat[A]] = + jsonCodecs.get(tag) map { _.asInstanceOf[JsonFormat[A]] } + def hasJsonCodec(tag: String): Boolean = + jsonCodecs.contains(tag) + def getOrElseUpdateJsonCodec[A](tag: String, v: JsonFormat[A]): JsonFormat[A] = + jsonCodecs.getOrElseUpdate(tag, v).asInstanceOf[JsonFormat[A]] + def stringCodec[A](tag: String): Option[ShowLines[A]] = + stringCodecs.get(tag) map { _.asInstanceOf[ShowLines[A]] } + def hasStringCodec(tag: String): Boolean = + stringCodecs.contains(tag) + def getOrElseUpdateStringCodec[A](tag: String, v: ShowLines[A]): ShowLines[A] = + stringCodecs.getOrElseUpdate(tag, v).asInstanceOf[ShowLines[A]] private[sbt] def buildAsyncStdout: AsyncAppender = { val ctx = XLogManager.getContext(false) match { case x: LoggerContext => x } diff --git a/internal/util-logging/src/test/scala/ManagedLoggerSpec.scala b/internal/util-logging/src/test/scala/ManagedLoggerSpec.scala index f7871053f..80e42a64d 100644 --- a/internal/util-logging/src/test/scala/ManagedLoggerSpec.scala +++ b/internal/util-logging/src/test/scala/ManagedLoggerSpec.scala @@ -20,6 +20,33 @@ class ManagedLoggerSpec extends FlatSpec with Matchers { log.infoEvent(1) } + it should "allow registering Show[Int]" in { + import sjsonnew.BasicJsonProtocol._ + val log = LogExchange.logger("foo") + LogExchange.bindLoggerAppenders("foo", List(LogExchange.asyncStdout -> Level.Info)) + implicit val intShow: ShowLines[Int] = ShowLines({ (x: Int) => Vector(s"String representation of $x") }) + log.registerStringCodec[Int] + log.infoEvent(1) + } + + it should "allow registering Show[Array[Int]]" in { + import sjsonnew.BasicJsonProtocol._ + val log = LogExchange.logger("foo") + LogExchange.bindLoggerAppenders("foo", List(LogExchange.asyncStdout -> Level.Info)) + implicit val intArrayShow: ShowLines[Array[Int]] = ShowLines({ (x: Array[Int]) => Vector(s"String representation of ${x.mkString}") }) + log.registerStringCodec[Array[Int]] + log.infoEvent(Array(1, 2, 3)) + } + + it should "allow registering Show[Vector[Vector[Int]]]" in { + import sjsonnew.BasicJsonProtocol._ + val log = LogExchange.logger("foo") + LogExchange.bindLoggerAppenders("foo", List(LogExchange.asyncStdout -> Level.Info)) + implicit val intVectorShow: ShowLines[Vector[Vector[Int]]] = ShowLines({ (xss: Vector[Vector[Int]]) => Vector(s"String representation of $xss") }) + log.registerStringCodec[Vector[Vector[Int]]] + log.infoEvent(Vector(Vector(1, 2, 3))) + } + "global logging" should "log immediately after initialization" in { // this is passed into State normally val global0 = initialGlobalLogging