diff --git a/akka-contrib/docs/jul.rst b/akka-contrib/docs/jul.rst new file mode 100644 index 0000000000..b0d1c0a668 --- /dev/null +++ b/akka-contrib/docs/jul.rst @@ -0,0 +1,17 @@ +Java Logging (JUL) +================= + +This extension module provides a logging backend which uses the `java.util.logging` (j.u.l) +API to do the endpoint logging for `akka.event.Logging`. + +Provided with this module is an implementation of `akka.event.LoggingAdapter` which is independent of any `ActorSystem` being in place. This means that j.u.l can be used as the backend, via the Akka Logging API, for both Actor and non-Actor codebases. + +To enable j.u.l as the `akka.event.Logging` backend, use the following Akka config: + + event-handlers = ["akka.contrib.jul.JavaLoggingEventHandler"] + +To access the `akka.event.Logging` API from non-Actor code, mix in `akka.contrib.jul.JavaLogging`. + +This module is preferred over SLF4J with its JDK14 backend, due to integration issues resulting in the incorrect handling of `threadId`, `className` and `methodName`. + +This extension module was contributed by Sam Halliday. \ No newline at end of file diff --git a/akka-contrib/src/main/scala/akka/contrib/jul/JulEventHandler.scala b/akka-contrib/src/main/scala/akka/contrib/jul/JulEventHandler.scala new file mode 100644 index 0000000000..68ce0ed973 --- /dev/null +++ b/akka-contrib/src/main/scala/akka/contrib/jul/JulEventHandler.scala @@ -0,0 +1,133 @@ +package akka.contrib.jul + +import akka.event.Logging._ +import akka.actor._ +import akka.event.LoggingAdapter +import java.util.logging +import concurrent.{ ExecutionContext, Future } + +/** + * Makes the Akka `Logging` API available as the `log` + * field, using `java.util.logging` as the backend. + * + * This trait does not require an `ActorSystem` and is + * encouraged to be used as a general purpose Scala + * logging API. + * + * For `Actor`s, use `ActorLogging` instead. + */ +trait JavaLogging { + + @transient + protected lazy val log = new JavaLoggingAdapter { + def logger = logging.Logger.getLogger(JavaLogging.this.getClass.getName) + } +} + +/** + * `java.util.logging` EventHandler. + */ +class JavaLoggingEventHandler extends Actor { + + def receive = { + case event @ Error(cause, logSource, logClass, message) ⇒ + log(logging.Level.SEVERE, cause, logSource, logClass, message, event) + + case event @ Warning(logSource, logClass, message) ⇒ + log(logging.Level.WARNING, null, logSource, logClass, message, event) + + case event @ Info(logSource, logClass, message) ⇒ + log(logging.Level.INFO, null, logSource, logClass, message, event) + + case event @ Debug(logSource, logClass, message) ⇒ + log(logging.Level.CONFIG, null, logSource, logClass, message, event) + + case InitializeLogger(_) ⇒ + sender ! LoggerInitialized + } + + @inline + def log(level: logging.Level, cause: Throwable, logSource: String, logClass: Class[_], message: Any, event: LogEvent) { + val logger = logging.Logger.getLogger(logSource) + val record = new logging.LogRecord(level, message.toString) + record.setLoggerName(logger.getName) + record.setThrown(cause) + record.setThreadID(event.thread.getId.toInt) + record.setSourceClassName(logClass.getName) + record.setSourceMethodName(null) // lost forever + logger.log(record) + } +} + +trait JavaLoggingAdapter extends LoggingAdapter { + + def logger: logging.Logger + + /** Override-able option for asynchronous logging */ + def loggingExecutionContext: Option[ExecutionContext] = None + + def isErrorEnabled = logger.isLoggable(logging.Level.SEVERE) + + def isWarningEnabled = logger.isLoggable(logging.Level.WARNING) + + def isInfoEnabled = logger.isLoggable(logging.Level.INFO) + + def isDebugEnabled = logger.isLoggable(logging.Level.CONFIG) + + protected def notifyError(message: String) { + log(logging.Level.SEVERE, null, message) + } + + protected def notifyError(cause: Throwable, message: String) { + log(logging.Level.SEVERE, cause, message) + } + + protected def notifyWarning(message: String) { + log(logging.Level.WARNING, null, message) + } + + protected def notifyInfo(message: String) { + log(logging.Level.INFO, null, message) + } + + protected def notifyDebug(message: String) { + log(logging.Level.CONFIG, null, message) + } + + @inline + def log(level: logging.Level, cause: Throwable, message: String) { + val record = new logging.LogRecord(level, message) + record.setLoggerName(logger.getName) + record.setThrown(cause) + updateSource(record) + + if (loggingExecutionContext.isDefined) { + implicit val context = loggingExecutionContext.get + Future(logger.log(record)).onFailure { + case thrown: Throwable ⇒ thrown.printStackTrace() + } + } else + logger.log(record) + } + + // it is unfortunate that this workaround is needed + private def updateSource(record: logging.LogRecord) { + val stack = Thread.currentThread.getStackTrace + val source = stack.find { + frame ⇒ + val cname = frame.getClassName + !cname.startsWith("akka.contrib.jul.") && + !cname.startsWith("akka.event.LoggingAdapter") && + !cname.startsWith("java.lang.reflect.") && + !cname.startsWith("sun.reflect.") + } + if (source.isDefined) { + record.setSourceClassName(source.get.getClassName) + record.setSourceMethodName(source.get.getMethodName) + } else { + record.setSourceClassName(null) + record.setSourceMethodName(null) + } + } + +} \ No newline at end of file diff --git a/akka-contrib/src/test/scala/akka/contrib/jul/JulEventHandlerSpec.scala b/akka-contrib/src/test/scala/akka/contrib/jul/JulEventHandlerSpec.scala new file mode 100644 index 0000000000..9508b75f8d --- /dev/null +++ b/akka-contrib/src/test/scala/akka/contrib/jul/JulEventHandlerSpec.scala @@ -0,0 +1,76 @@ +package akka.contrib.jul + +import com.typesafe.config.ConfigFactory +import akka.actor.{ ActorSystem, Actor, ActorLogging, Props } +import akka.testkit.AkkaSpec +import java.util.logging +import java.io.ByteArrayInputStream + +object JavaLoggingEventHandlerSpec { + + val config = ConfigFactory.parseString(""" + akka { + loglevel = INFO + event-handlers = ["akka.contrib.jul.JavaLoggingEventHandler"] + }""") + + class LogProducer extends Actor with ActorLogging { + def receive = { + case e: Exception ⇒ + log.error(e, e.getMessage) + case (s: String, x: Int) ⇒ + log.info(s, x) + } + } +} + +@org.junit.runner.RunWith(classOf[org.scalatest.junit.JUnitRunner]) +class JavaLoggingEventHandlerSpec extends AkkaSpec(JavaLoggingEventHandlerSpec.config) { + + val logger = logging.Logger.getLogger("akka://JavaLoggingEventHandlerSpec/user/log") + logger.setUseParentHandlers(false) // turn off output of test LogRecords + logger.addHandler(new logging.Handler { + def publish(record: logging.LogRecord) { + testActor ! record + } + + def flush() {} + def close() {} + }) + + val producer = system.actorOf(Props[JavaLoggingEventHandlerSpec.LogProducer], name = "log") + + "JavaLoggingEventHandler" must { + + "log error with stackTrace" in { + producer ! new RuntimeException("Simulated error") + + val record = expectMsgType[logging.LogRecord] + + record must not be (null) + record.getMillis must not be (0) + record.getThreadID must not be (0) + record.getLevel must be(logging.Level.SEVERE) + record.getMessage must be("Simulated error") + record.getThrown.isInstanceOf[RuntimeException] must be(true) + record.getSourceClassName must be("akka.contrib.jul.JavaLoggingEventHandlerSpec$LogProducer") + record.getSourceMethodName must be(null) + } + + "log info without stackTrace" in { + producer ! ("{} is the magic number", 3) + + val record = expectMsgType[logging.LogRecord] + + record must not be (null) + record.getMillis must not be (0) + record.getThreadID must not be (0) + record.getLevel must be(logging.Level.INFO) + record.getMessage must be("3 is the magic number") + record.getThrown must be(null) + record.getSourceClassName must be("akka.contrib.jul.JavaLoggingEventHandlerSpec$LogProducer") + record.getSourceMethodName must be(null) + } + } + +}