Permalink
Browse files

Beta version of port to Scala.JS

This is a beta version of E2E Scala.JS support in Log4s. The JS
interfaces and APIs are still considered experimental, but the build
is successful and it should be possible to do basic logging and
configuration through Scala.JS.
  • Loading branch information...
sarahgerweck committed Mar 3, 2018
1 parent d47503d commit 33e44d476bc1695bc16c73c7a2b0c4fa327327f8
Showing with 842 additions and 17 deletions.
  1. +27 −9 build.sbt
  2. +20 −0 core/js/src/main/scala/org/log4s/log4sjs/CategoryParser.scala
  3. +18 −0 core/js/src/main/scala/org/log4s/log4sjs/ExceptionInfo.scala
  4. +42 −0 core/js/src/main/scala/org/log4s/log4sjs/FunctionalType.scala
  5. +10 −0 core/js/src/main/scala/org/log4s/log4sjs/Log4s.scala
  6. +20 −0 core/js/src/main/scala/org/log4s/log4sjs/Log4sAppender.scala
  7. +170 −0 core/js/src/main/scala/org/log4s/log4sjs/Log4sConfig.scala
  8. +19 −0 core/js/src/main/scala/org/log4s/log4sjs/Log4sConsoleAppender.scala
  9. +48 −0 core/js/src/main/scala/org/log4s/log4sjs/Log4sLoggerFactory.scala
  10. +43 −0 core/js/src/main/scala/org/log4s/log4sjs/Log4sMDC.scala
  11. +29 −0 core/js/src/main/scala/org/log4s/log4sjs/LoggedEvent.scala
  12. +132 −0 core/js/src/main/scala/org/log4s/log4sjs/MessageFormatter.scala
  13. +5 −0 core/js/src/main/scala/org/slf4j/ILoggerFactory.scala
  14. +38 −0 core/js/src/main/scala/org/slf4j/Logger.scala
  15. +7 −0 core/js/src/main/scala/org/slf4j/LoggerFactory.scala
  16. +52 −0 core/js/src/main/scala/org/slf4j/MDC.scala
  17. +79 −0 core/js/src/test/scala/org/log4s/PlatformInit.scala
  18. +1 −1 core/{ → jvm}/src/test/resources/logback-test.xml
  19. +5 −0 core/jvm/src/test/scala/org/log4s/PlatformInit.scala
  20. 0 core/{ → shared}/src/main/scala-2.10/org/log4s/LoggerMacros.scala
  21. 0 core/{ → shared}/src/main/scala-2.11/org/log4s/LoggerMacros.scala
  22. +9 −1 core/{ → shared}/src/main/scala/org/log4s/LogLevel.scala
  23. 0 core/{ → shared}/src/main/scala/org/log4s/Logger.scala
  24. 0 core/{ → shared}/src/main/scala/org/log4s/MDC.scala
  25. 0 core/{ → shared}/src/main/scala/org/log4s/package.scala
  26. 0 core/{ → shared}/src/test/scala/org/log4s/GetLoggerSpec.scala
  27. +5 −0 core/shared/src/test/scala/org/log4s/IPlatformInit.scala
  28. +1 −0 core/{ → shared}/src/test/scala/org/log4s/LoggerInit.scala
  29. +0 −3 core/{ → shared}/src/test/scala/org/log4s/LoggerSpec.scala
  30. 0 core/{ → shared}/src/test/scala/org/log4s/MDCSpec.scala
  31. +5 −3 project/Dependencies.scala
  32. +1 −0 project/plugin-scalajs.sbt
  33. +56 −0 testing/js/src/main/scala/ch/qos/logback.scala
  34. 0 testing/{ → shared}/src/main/scala/org/log4s/LoggedEvent.scala
  35. 0 testing/{ → shared}/src/main/scala/org/log4s/LoggedThrowable.scala
  36. 0 testing/{ → shared}/src/main/scala/org/log4s/TestAppender.scala
View
@@ -22,11 +22,16 @@ releaseProcess := Seq[ReleaseStep](
pushChanges
)
def jsOpts = new Def.SettingList(Seq(
scalacOptions += "-P:scalajs:sjsDefinedByDefault",
scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.CommonJSModule) }
))
lazy val root: Project = (project in file ("."))
.enablePlugins(BasicSettings)
.settings(Publish.settings: _*)
.settings(Release.settings: _*)
.aggregate(core, testing)
.aggregate(coreJVM, coreJS, testingJVM, testingJS)
.settings (
name := "Log4s Root",
@@ -40,7 +45,7 @@ lazy val root: Project = (project in file ("."))
skip in Test := true
)
lazy val core: Project = (project in file ("core"))
lazy val core = (crossProject in file ("core"))
.enablePlugins(BasicSettings, SiteSettingsPlugin)
.dependsOn(testing % "test")
.settings(Publish.settings: _*)
@@ -76,20 +81,29 @@ lazy val core: Project = (project in file ("core"))
libraryDependencies ++= Seq (
slf4j,
logback % "test",
scalatest % "test",
reflect(scalaVersion.value) % "provided"
logback % "test",
"org.scalatest" %%% "scalatest" % scalatestVersion % "test",
reflect(scalaVersion.value) % "provided"
),
unmanagedSourceDirectories in Compile += {
unmanagedSourceDirectories in Compile ++= {
scalaBinaryVersion.value match {
case "2.10" => baseDirectory.value / "src" / "main" / "scala-2.10"
case _ => baseDirectory.value / "src" / "main" / "scala-2.11"
case "2.10" | "2.11" =>
Seq.empty
case _ =>
Seq(baseDirectory.value / ".." / "shared" / "src" / "main" / "scala-2.11")
}
}
)
.jvmSettings(
libraryDependencies += "org.scala-js" %% "scalajs-stubs" % scalaJSVersion % "provided"
)
.jsSettings(jsOpts)
lazy val testing: Project = (project in file ("testing"))
lazy val coreJS = core.js
lazy val coreJVM = core.jvm
lazy val testing = (crossProject in file ("testing"))
.enablePlugins(BasicSettings, SiteSettingsPlugin)
.settings(Publish.settings: _*)
.settings(Release.settings: _*)
@@ -102,3 +116,7 @@ lazy val testing: Project = (project in file ("testing"))
logback
)
)
.jsSettings(jsOpts)
lazy val testingJS = testing.js
lazy val testingJVM = testing.jvm
@@ -0,0 +1,20 @@
package org.log4s.log4sjs
private[log4sjs] object CategoryParser {
/* TODO: Handle category escaping? Don't parse the full string if not needed? */
def apply(category: String): Seq[String] = {
category
.foldLeft(Nil: List[String]) { (l, c) =>
c match {
case '.' =>
"" :: l
case oc =>
l match {
case h :: t => h + oc :: t
case Nil => oc.toString :: Nil
}
}
}
.reverse
}
}
@@ -0,0 +1,18 @@
package org.log4s
package log4sjs
import scala.scalajs.js
sealed abstract class ExceptionInfo extends js.Object
object ExceptionInfo {
def apply(t: Throwable): ExceptionInfo = {
Option(t).map(new ThrowableException(_)).getOrElse(NoException)
}
def apply(e: js.Error): ExceptionInfo = {
Option(e).map(new JsErrorException(_)).getOrElse(NoException)
}
object NoException extends ExceptionInfo
class ThrowableException(val throwable: Throwable) extends ExceptionInfo
class JsErrorException(val error: js.Error) extends ExceptionInfo
}
@@ -0,0 +1,42 @@
package org.log4s
package log4sjs
import scala.scalajs.js
abstract class FunctionalType[ObjectType <: js.Any, Argument, Result]
(val typeName: String, dynamicField: Symbol) { thisFT =>
type Type = ObjectType
type FunctionType = js.Function1[Argument, Result]
type DynamicType = js.|[FunctionType, Type]
def fromDynamicType(formatter: DynamicType): Type = {
val dynamic = formatter.merge.asInstanceOf[js.Dynamic]
if (dynamic.selectDynamic(dynamicField.name).isInstanceOf[js.Function]) {
from(dynamic.asInstanceOf[Type])
} else if (dynamic.isInstanceOf[js.Function]) {
from(dynamic.asInstanceOf[FunctionType])
} else {
throw new IllegalArgumentException(s"$typeName object has no $dynamicField and is not a function: $dynamic")
}
}
protected[this] def fromFunction(fn: Argument => Result): Type
@inline
final def from[A](a: A)(implicit p: Provider[A]) = p(a)
trait Provider[A] {
def apply(a: A): Type
}
object Provider {
private[this] def fromFunction[A](f: A => Type): Provider[A] =
new Provider[A] { def apply(a: A) = f(a) }
implicit val identityProvider: Provider[Type] = fromFunction(identity)
implicit val functionProvider: Provider[Argument => Result] =
fromFunction(thisFT.fromFunction)
implicit val jsFunctionProvider: Provider[FunctionType] =
fromFunction(thisFT.fromFunction(_))
implicit val dynamicProvider: Provider[DynamicType] =
fromFunction(fromDynamicType)
}
}
@@ -0,0 +1,10 @@
package org.log4s
package log4sjs
import scala.scalajs.js
import js.annotation._
object Log4s {
@JSExportTopLevel("getLogger")
def getLogger(name: String) = org.slf4j.LoggerFactory.getLogger(name)
}
@@ -0,0 +1,20 @@
package org.log4s
package log4sjs
import scala.scalajs.js
trait Log4sAppender extends js.Any {
def append(event: LoggedEvent): Unit
}
object Log4sAppender extends FunctionalType[Log4sAppender, LoggedEvent, Unit]("Appender", 'append) {
protected[this] def fromFunction(fn: LoggedEvent => Unit) = {
new js.Object with Log4sAppender {
override def append(le: LoggedEvent): Unit = { fn(le) }
}
}
def consoleAppender[A: MessageFormatter.Provider](mf: A): Log4sAppender = {
new Log4sConsoleAppender(MessageFormatter.from(mf))
}
}
@@ -0,0 +1,170 @@
package org.log4s
package log4sjs
import scala.annotation.tailrec
import scala.collection.{ breakOut, mutable, immutable }
import scala.math.Ordered._
import scala.scalajs.js
import js.annotation._
case class AppenderSetting(appenders: immutable.Seq[Log4sAppender.Type], additive: Boolean)
object Log4sConfig { thisConfig =>
private[this] lazy val standardAppender = new Log4sConsoleAppender
private[this] lazy val defaultAppenderSetting = AppenderSetting(Nil, true)
private[this] case class ConcreteLoggerState(threshold: LogLevel, appenders: Iterable[Log4sAppender]) {
def withChild(ls: LoggerState): ConcreteLoggerState = {
val newThreshold = ls.threshold.getOrElse(threshold)
val newAppenders = {
val AppenderSetting(childAppenders, childAdditive) = ls.appenders.getOrElse(defaultAppenderSetting)
if (childAdditive) {
appenders ++ childAppenders
} else {
childAppenders
}
}
ConcreteLoggerState(newThreshold, newAppenders)
}
def isEnabled(ll: LogLevel): Boolean = {
ll >= threshold
}
}
private[this] case class LoggerState(threshold: Option[LogLevel] = None, appenders: Option[AppenderSetting] = None)
private[this] lazy val emptyLoggerState = LoggerState()
private[this] class Node(
val children: mutable.Map[String, Node] = mutable.Map.empty,
var state: LoggerState = emptyLoggerState)
private[this] object LoggerState {
private[this] val defaultRootState = ConcreteLoggerState(Trace, Seq(standardAppender))
private[this] val root = new Node()
def apply(parts: Seq[String]): ConcreteLoggerState = {
@inline
@tailrec
def helper(tree: collection.Map[String, Node], state: ConcreteLoggerState, path: Seq[String]): ConcreteLoggerState = {
if (path.isEmpty) {
state
} else {
tree.get(path.head) match {
case None => state
case Some(n) => helper(n.children, state.withChild(n.state), path.tail)
}
}
}
helper(root.children, defaultRootState.withChild(root.state), parts)
}
@tailrec
protected[this] def getNode(parts: Seq[String], node: Node = root): Option[Node] = {
/* This method can be written in one line as a left fold that does a `return None` in a
* `getOrElse`. However the early return is unusual and it may not perform very well to
* throw an exception in the most common case, so the tail-recursive form is fine. */
if (parts.isEmpty) {
Some(node)
} else {
node.children.get(parts.head) match {
case None => None
case Some(n2) => getNode(parts.tail, n2)
}
}
}
def get(parts: Seq[String]): LoggerState = {
getNode(parts)
.map(_.state)
.getOrElse(emptyLoggerState)
}
def update(parts: Seq[String], state: LoggerState): Unit = {
val pathNode =
parts.foldLeft(root) { (node, part) =>
node.children.getOrElseUpdate(part, new Node())
}
/* TODO: Merge settings rather than pure overwrite? */
pathNode.state = state
}
}
def logger(name: String, threshold: Option[Option[LogLevel]] = None, appenders: Option[Option[AppenderSetting]] = None): Unit = {
val parts = CategoryParser(name)
val currentState = LoggerState.get(parts)
var updatedState = currentState
for (t <- threshold) {
updatedState = updatedState.copy(threshold = t)
}
for (a <- appenders) {
updatedState = updatedState.copy(appenders = a)
}
LoggerState.update(parts, updatedState)
}
@JSExportTopLevel("Config.setLoggerThreshold")
def setLoggerThreshold(name: String, threshold: LogLevel): Unit = {
logger(name, threshold = Some(Option(threshold)))
}
@JSExportTopLevel("Config.setLoggerThreshold")
def setLoggerThreshold(name: String, threshold: String): Unit = {
setLoggerThreshold(name, LogLevel.forName(threshold))
}
@JSExportTopLevel("Config.setCategoryAppenders")
def setCategoryAppendersDynamic(name: String, additive: Boolean, appenders: js.Array[Log4sAppender.DynamicType]): Unit = {
setCategoryAppenders(name, additive, appenders)
}
def setCategoryAppenders[A: Log4sAppender.Provider](name: String, additive: Boolean, appenders: Seq[A]): Unit = {
val appenderSeq: immutable.Seq[Log4sAppender] = appenders.map(Log4sAppender.from(_))(breakOut)
logger(name, appenders = Some(Some(AppenderSetting(appenderSeq, additive))))
}
/** Add an appender for a given category */
@JSExportTopLevel("Config.addCategoryAppender")
def addCategoryAppenderDynamic(name: String, appender: Log4sAppender.DynamicType): Unit = {
addCategoryAppender(name, appender)
}
def addCategoryAppender[A: Log4sAppender.Provider](name: String, appender: A): Unit = {
val parts = CategoryParser(name)
val currentState = LoggerState.get(parts)
val currentAppenderSetting = currentState.appenders.getOrElse(AppenderSetting(Nil, true))
val newAppenderSetting = currentAppenderSetting.copy(appenders = currentAppenderSetting.appenders :+ Log4sAppender.from(appender))
val updatedState = currentState.copy(appenders = Some(newAppenderSetting))
LoggerState(parts) = updatedState
}
private[log4sjs] final def doLog(e: LoggedEvent): Unit = {
val state = LoggerState(e.loggerPath)
if (state.isEnabled(e.level)) {
for (appender <- state.appenders) {
appender.append(e)
}
}
}
private[log4sjs] final def isPathEnabled(path: Seq[String], ll: LogLevel): Boolean = {
LoggerState(path).isEnabled(ll)
}
private[log4sjs] final def isNameEnabled(name: String, ll: LogLevel): Boolean =
isPathEnabled(CategoryParser(name), ll)
private[this] def levelNumber(ll: LogLevel): Byte = ll match {
case Trace => 1
case Debug => 2
case Info => 3
case Warn => 4
case Error => 5
}
private[this] implicit val logLevelOrdering: Ordering[LogLevel] = Ordering by levelNumber
}
@@ -0,0 +1,19 @@
package org.log4s
package log4sjs
import scala.scalajs.js
import js.Dynamic.{ global => g }
class Log4sConsoleAppender(val formatter: MessageFormatter = new StandardMessageFormatter()) extends js.Object with Log4sAppender {
def append(event: LoggedEvent): Unit = {
val formatted = formatter.render(event)
event.level match {
case Trace | Debug | Info =>
g.console.log(formatted)
case Warn =>
g.console.warn(formatted)
case Error =>
g.console.error(formatted)
}
}
}
Oops, something went wrong.

0 comments on commit 33e44d4

Please sign in to comment.