diff --git a/akka-actor-typed-tests/src/test/java/akka/actor/typed/javadsl/ActorLoggingTest.java b/akka-actor-typed-tests/src/test/java/akka/actor/typed/javadsl/ActorLoggingTest.java new file mode 100644 index 0000000000..034151bb77 --- /dev/null +++ b/akka-actor-typed-tests/src/test/java/akka/actor/typed/javadsl/ActorLoggingTest.java @@ -0,0 +1,74 @@ +/** + * Copyright (C) 2009-2018 Lightbend Inc. + */ +package akka.actor.typed.javadsl; + +import akka.actor.typed.Behavior; +import akka.event.Logging; +import akka.japi.pf.PFBuilder; +import akka.testkit.CustomEventFilter; +import akka.testkit.typed.TestKit; +import com.typesafe.config.ConfigFactory; +import org.junit.AfterClass; +import org.junit.Test; +import org.scalatest.junit.JUnitSuite; +import scala.concurrent.duration.FiniteDuration; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class ActorLoggingTest extends JUnitSuite { + + private final static TestKit testKit = new TestKit("ActorLoggingTest", + ConfigFactory.parseString( + "akka.loglevel = INFO\n" + + "akka.loggers = [\"akka.testkit.TestEventListener\"]" + )); + + @AfterClass + public static void tearDown() { + testKit.shutdown(); + } + + interface Protocol { + String getTransactionId(); + } + + static class Message implements Protocol { + public final String transactionId; + public Message(String transactionId) { + this.transactionId = transactionId; + } + public String getTransactionId() { + return transactionId; + } + } + + @Test + public void loggingProvidesMDC() { + Behavior behavior = Behaviors.withMdc( + Protocol.class, + (msg) -> { + Map mdc = new HashMap<>(); + mdc.put("txId", msg.getTransactionId()); + return mdc; + }, + Behaviors.immutable(Protocol.class) + .onMessage(Message.class, (ctx, msg) -> { + ctx.getLog().info(msg.toString()); + return Behaviors.same(); + }).build() + ); + + CustomEventFilter eventFilter = new CustomEventFilter(new PFBuilder() + .match(Logging.LogEvent.class, (event) -> + event.getMDC().containsKey("txId")) + .build(), + 1); + + testKit.spawn(behavior); + eventFilter.awaitDone(FiniteDuration.create(3, TimeUnit.SECONDS)); + + } +} diff --git a/akka-actor-typed-tests/src/test/scala/akka/actor/typed/ActorContextSpec.scala b/akka-actor-typed-tests/src/test/scala/akka/actor/typed/ActorContextSpec.scala index f1dafbac5a..8331374d0c 100644 --- a/akka-actor-typed-tests/src/test/scala/akka/actor/typed/ActorContextSpec.scala +++ b/akka-actor-typed-tests/src/test/scala/akka/actor/typed/ActorContextSpec.scala @@ -308,14 +308,15 @@ abstract class ActorContextSpec extends TypedAkkaSpec { import ActorContextSpec._ val config = ConfigFactory.parseString( - """|akka { - | loglevel = WARNING - | actor.debug { - | lifecycle = off - | autoreceive = off - | } - | typed.loggers = ["akka.testkit.typed.TestEventListener"] - |}""".stripMargin) + """ + akka { + loglevel = WARNING + loggers = ["akka.testkit.TestEventListener"] + actor.debug { + lifecycle = off + autoreceive = off + } + }""") implicit lazy val system: ActorSystem[GuardianCommand] = ActorSystem(guardian(), AkkaSpec.getCallerName(classOf[ActorContextSpec]), config = Some(config withFallback AkkaSpec.testConf)) @@ -379,7 +380,8 @@ abstract class ActorContextSpec extends TypedAkkaSpec { pattern: String = null, occurrences: Int = Int.MaxValue)(implicit system: ActorSystem[GuardianCommand]): EventFilter = { val filter = EventFilter(message, source, start, pattern, occurrences) - system.eventStream.publish(Mute(filter)) + import scaladsl.adapter._ + system.toUntyped.eventStream.publish(Mute(filter)) filter } diff --git a/akka-actor-typed-tests/src/test/scala/akka/actor/typed/scaladsl/ActorLoggingSpec.scala b/akka-actor-typed-tests/src/test/scala/akka/actor/typed/scaladsl/ActorLoggingSpec.scala new file mode 100644 index 0000000000..522d7bfd2e --- /dev/null +++ b/akka-actor-typed-tests/src/test/scala/akka/actor/typed/scaladsl/ActorLoggingSpec.scala @@ -0,0 +1,228 @@ +/** + * Copyright (C) 2009-2018 Lightbend Inc. + */ +package akka.actor.typed.scaladsl + +import akka.actor.typed.{ LogMarker, TestException, TypedAkkaSpec, scaladsl } +import akka.testkit.EventFilter +import akka.testkit.typed.TestKit +import com.typesafe.config.ConfigFactory +import akka.actor.typed.scaladsl.adapter._ +import akka.event.Logging +import akka.event.Logging.{ LogEventWithCause, LogEventWithMarker } + +import scala.util.control.NoStackTrace + +class ActorLoggingSpec extends TestKit(ConfigFactory.parseString( + """ + akka.loglevel = DEBUG + akka.loggers = ["akka.testkit.TestEventListener"] + """)) with TypedAkkaSpec { + + val marker = LogMarker("marker") + val cause = new TestException("böö") + + implicit val untyped = system.toUntyped + + "Logging in a typed actor" must { + + "be conveniently available from the ctx" in { + val actor = EventFilter.info("Started", source = "akka://ActorLoggingSpec/user/the-actor", occurrences = 1).intercept { + spawn(Behaviors.deferred[String] { ctx ⇒ + ctx.log.info("Started") + + Behaviors.immutable { (ctx, msg) ⇒ + ctx.log.info("got message {}", msg) + Behaviors.same + } + }, "the-actor") + } + + EventFilter.info("got message Hello", source = "akka://ActorLoggingSpec/user/the-actor", occurrences = 1).intercept { + actor ! "Hello" + } + } + + "pass markers to the log" in { + EventFilter.custom({ + case event: LogEventWithMarker if event.marker == marker ⇒ true + }, occurrences = 5).intercept( + spawn(Behaviors.deferred[Any] { ctx ⇒ + ctx.log.debug(marker, "whatever") + ctx.log.info(marker, "whatever") + ctx.log.warning(marker, "whatever") + ctx.log.error(marker, "whatever") + ctx.log.error(marker, cause, "whatever") + Behaviors.stopped + }) + ) + } + + "pass cause with warning" in { + EventFilter.custom({ + case event: LogEventWithCause if event.cause == cause ⇒ true + }, occurrences = 2).intercept( + spawn(Behaviors.deferred[Any] { ctx ⇒ + ctx.log.warning(cause, "whatever") + ctx.log.warning(marker, cause, "whatever") + Behaviors.stopped + }) + ) + } + + "provide a whole bunch of logging overloads" in { + + // Not the best test but at least it exercises every log overload ;) + + EventFilter.custom({ + case _ ⇒ true // any is fine, we're just after the right count of statements reaching the listener + }, occurrences = 72).intercept { + spawn(Behaviors.deferred[String] { ctx ⇒ + ctx.log.debug("message") + ctx.log.debug("{}", "arg1") + ctx.log.debug("{} {}", "arg1", "arg2") + ctx.log.debug("{} {} {}", "arg1", "arg2", "arg3") + ctx.log.debug("{} {} {} {}", "arg1", "arg2", "arg3", "arg4") + ctx.log.debug("{} {} {} {} {}", Array("arg1", "arg2", "arg3", "arg4", "arg5")) + ctx.log.debug(marker, "message") + ctx.log.debug(marker, "{}", "arg1") + ctx.log.debug(marker, "{} {}", "arg1", "arg2") + ctx.log.debug(marker, "{} {} {}", "arg1", "arg2", "arg3") + ctx.log.debug(marker, "{} {} {} {}", "arg1", "arg2", "arg3", "arg4") + ctx.log.debug(marker, "{} {} {} {} {}", Array("arg1", "arg2", "arg3", "arg4", "arg5")) + + ctx.log.info("message") + ctx.log.info("{}", "arg1") + ctx.log.info("{} {}", "arg1", "arg2") + ctx.log.info("{} {} {}", "arg1", "arg2", "arg3") + ctx.log.info("{} {} {} {}", "arg1", "arg2", "arg3", "arg4") + ctx.log.info("{} {} {} {} {}", Array("arg1", "arg2", "arg3", "arg4", "arg5")) + ctx.log.info(marker, "message") + ctx.log.info(marker, "{}", "arg1") + ctx.log.info(marker, "{} {}", "arg1", "arg2") + ctx.log.info(marker, "{} {} {}", "arg1", "arg2", "arg3") + ctx.log.info(marker, "{} {} {} {}", "arg1", "arg2", "arg3", "arg4") + ctx.log.info(marker, "{} {} {} {} {}", Array("arg1", "arg2", "arg3", "arg4", "arg5")) + + ctx.log.warning("message") + ctx.log.warning("{}", "arg1") + ctx.log.warning("{} {}", "arg1", "arg2") + ctx.log.warning("{} {} {}", "arg1", "arg2", "arg3") + ctx.log.warning("{} {} {} {}", "arg1", "arg2", "arg3", "arg4") + ctx.log.warning("{} {} {} {} {}", Array("arg1", "arg2", "arg3", "arg4", "arg5")) + ctx.log.warning(marker, "message") + ctx.log.warning(marker, "{}", "arg1") + ctx.log.warning(marker, "{} {}", "arg1", "arg2") + ctx.log.warning(marker, "{} {} {}", "arg1", "arg2", "arg3") + ctx.log.warning(marker, "{} {} {} {}", "arg1", "arg2", "arg3", "arg4") + ctx.log.warning(marker, "{} {} {} {} {}", Array("arg1", "arg2", "arg3", "arg4", "arg5")) + + ctx.log.warning(cause, "message") + ctx.log.warning(cause, "{}", "arg1") + ctx.log.warning(cause, "{} {}", "arg1", "arg2") + ctx.log.warning(cause, "{} {} {}", "arg1", "arg2", "arg3") + ctx.log.warning(cause, "{} {} {} {}", "arg1", "arg2", "arg3", "arg4") + ctx.log.warning(cause, "{} {} {} {} {}", Array("arg1", "arg2", "arg3", "arg4", "arg5")) + ctx.log.warning(marker, cause, "message") + ctx.log.warning(marker, cause, "{}", "arg1") + ctx.log.warning(marker, cause, "{} {}", "arg1", "arg2") + ctx.log.warning(marker, cause, "{} {} {}", "arg1", "arg2", "arg3") + ctx.log.warning(marker, cause, "{} {} {} {}", "arg1", "arg2", "arg3", "arg4") + ctx.log.warning(marker, cause, "{} {} {} {} {}", Array("arg1", "arg2", "arg3", "arg4", "arg5")) + + ctx.log.error("message") + ctx.log.error("{}", "arg1") + ctx.log.error("{} {}", "arg1", "arg2") + ctx.log.error("{} {} {}", "arg1", "arg2", "arg3") + ctx.log.error("{} {} {} {}", "arg1", "arg2", "arg3", "arg4") + ctx.log.error("{} {} {} {} {}", Array("arg1", "arg2", "arg3", "arg4", "arg5")) + ctx.log.error(marker, "message") + ctx.log.error(marker, "{}", "arg1") + ctx.log.error(marker, "{} {}", "arg1", "arg2") + ctx.log.error(marker, "{} {} {}", "arg1", "arg2", "arg3") + ctx.log.error(marker, "{} {} {} {}", "arg1", "arg2", "arg3", "arg4") + ctx.log.error(marker, "{} {} {} {} {}", Array("arg1", "arg2", "arg3", "arg4", "arg5")) + + ctx.log.error(cause, "message") + ctx.log.error(cause, "{}", "arg1") + ctx.log.error(cause, "{} {}", "arg1", "arg2") + ctx.log.error(cause, "{} {} {}", "arg1", "arg2", "arg3") + ctx.log.error(cause, "{} {} {} {}", "arg1", "arg2", "arg3", "arg4") + ctx.log.error(cause, "{} {} {} {} {}", Array("arg1", "arg2", "arg3", "arg4", "arg5")) + ctx.log.error(marker, cause, "message") + ctx.log.error(marker, cause, "{}", "arg1") + ctx.log.error(marker, cause, "{} {}", "arg1", "arg2") + ctx.log.error(marker, cause, "{} {} {}", "arg1", "arg2", "arg3") + ctx.log.error(marker, cause, "{} {} {} {}", "arg1", "arg2", "arg3", "arg4") + ctx.log.error(marker, cause, "{} {} {} {} {}", Array("arg1", "arg2", "arg3", "arg4", "arg5")) + + Behaviors.stopped + }) + } + } + + } + + trait Protocol { + def transactionId: Long + } + case class Message(transactionId: Long, message: String) extends Protocol + + "Logging with MDC for a typed actor" must { + + "provide the MDC values in the log" in { + val behaviors = Behaviors.withMdc[Protocol]( + { (msg) ⇒ + if (msg.transactionId == 1) + Map( + "txId" -> msg.transactionId, + "first" -> true + ) + else Map("txId" -> msg.transactionId) + }, + Behaviors.deferred { ctx ⇒ + ctx.log.info("Starting") + Behaviors.immutable { (ctx, msg) ⇒ + ctx.log.info("Got message!") + Behaviors.same + } + } + ) + + // mdc on defer is empty (thread and timestamp MDC is added by logger backend) + val ref = EventFilter.custom({ + case logEvent if logEvent.level == Logging.InfoLevel ⇒ + logEvent.message should ===("Starting") + logEvent.mdc shouldBe empty + true + case other ⇒ system.log.error(s"Unexpected log event: {}", other); false + }, occurrences = 1).intercept { + spawn(behaviors) + } + + // mdc on message + EventFilter.custom({ + case logEvent if logEvent.level == Logging.InfoLevel ⇒ + logEvent.message should ===("Got message!") + logEvent.mdc should ===(Map("txId" -> 1L, "first" -> true)) + true + case other ⇒ system.log.error(s"Unexpected log event: {}", other); false + }, occurrences = 1).intercept { + ref ! Message(1, "first") + } + + // mdc does not leak between messages + EventFilter.custom({ + case logEvent if logEvent.level == Logging.InfoLevel ⇒ + logEvent.message should ===("Got message!") + logEvent.mdc should ===(Map("txId" -> 2L)) + true + case other ⇒ system.log.error(s"Unexpected log event: {}", other); false + }, occurrences = 1).intercept { + ref ! Message(2, "second") + } + } + + } + +} diff --git a/akka-actor-typed-tests/src/test/scala/akka/actor/typed/scaladsl/StashSpec.scala b/akka-actor-typed-tests/src/test/scala/akka/actor/typed/scaladsl/StashSpec.scala index 920c687f24..33f15f2a71 100644 --- a/akka-actor-typed-tests/src/test/scala/akka/actor/typed/scaladsl/StashSpec.scala +++ b/akka-actor-typed-tests/src/test/scala/akka/actor/typed/scaladsl/StashSpec.scala @@ -19,9 +19,6 @@ object StashSpec { final case class GetProcessed(replyTo: ActorRef[Vector[String]]) extends Command final case class GetStashSize(replyTo: ActorRef[Int]) extends Command - // FIXME replace when we get the logging in place, #23326 - def log(ctx: ActorContext[_]): LoggingAdapter = ctx.system.log - def active(processed: Vector[String]): Behavior[Command] = Behaviors.immutable { (ctx, cmd) ⇒ cmd match { @@ -57,13 +54,13 @@ object StashSpec { case UnstashAll ⇒ buffer.unstashAll(ctx, active(processed)) case Unstash ⇒ - log(ctx).debug(s"Unstash ${buffer.size}") + ctx.log.debug(s"Unstash ${buffer.size}") if (buffer.isEmpty) active(processed) else { ctx.self ! Unstash // continue unstashing until buffer is empty val numberOfMessages = 2 - log(ctx).debug(s"Unstash $numberOfMessages of ${buffer.size}, starting with ${buffer.head}") + ctx.log.debug(s"Unstash $numberOfMessages of ${buffer.size}, starting with ${buffer.head}") buffer.unstash(ctx, unstashing(buffer.drop(numberOfMessages), processed), numberOfMessages, Unstashed) } case Stash ⇒ @@ -77,28 +74,28 @@ object StashSpec { Behaviors.immutable { (ctx, cmd) ⇒ cmd match { case Unstashed(msg: Msg) ⇒ - log(ctx).debug(s"unstashed $msg") + ctx.log.debug(s"unstashed $msg") unstashing(buffer, processed :+ msg.s) case Unstashed(GetProcessed(replyTo)) ⇒ - log(ctx).debug(s"unstashed GetProcessed") + ctx.log.debug(s"unstashed GetProcessed") replyTo ! processed Behaviors.same case msg: Msg ⇒ - log(ctx).debug(s"got $msg in unstashing") + ctx.log.debug(s"got $msg in unstashing") unstashing(buffer :+ msg, processed) case get: GetProcessed ⇒ - log(ctx).debug(s"got GetProcessed in unstashing") + ctx.log.debug(s"got GetProcessed in unstashing") unstashing(buffer :+ get, processed) case Stash ⇒ stashing(buffer, processed) case Unstash ⇒ if (buffer.isEmpty) { - log(ctx).debug(s"unstashing done") + ctx.log.debug(s"unstashing done") active(processed) } else { ctx.self ! Unstash // continue unstashing until buffer is empty val numberOfMessages = 2 - log(ctx).debug(s"Unstash $numberOfMessages of ${buffer.size}, starting with ${buffer.head}") + ctx.log.debug(s"Unstash $numberOfMessages of ${buffer.size}, starting with ${buffer.head}") buffer.unstash(ctx, unstashing(buffer.drop(numberOfMessages), processed), numberOfMessages, Unstashed) } case GetStashSize(replyTo) ⇒ @@ -147,15 +144,15 @@ object StashSpec { } else { ctx.self ! Unstash // continue unstashing until buffer is empty val numberOfMessages = 2 - log(ctx).debug(s"Unstash $numberOfMessages of ${buffer.size}, starting with ${buffer.head}") + ctx.log.debug(s"Unstash $numberOfMessages of ${buffer.size}, starting with ${buffer.head}") buffer.unstash(ctx, this, numberOfMessages, Unstashed) } case Unstashed(msg: Msg) ⇒ - log(ctx).debug(s"unstashed $msg") + ctx.log.debug(s"unstashed $msg") processed :+= msg.s this case Unstashed(GetProcessed(replyTo)) ⇒ - log(ctx).debug(s"unstashed GetProcessed") + ctx.log.debug(s"unstashed GetProcessed") replyTo ! processed Behaviors.same case _: Unstashed ⇒ diff --git a/akka-actor-typed-tests/src/test/scala/docs/akka/typed/GracefulStopDocSpec.scala b/akka-actor-typed-tests/src/test/scala/docs/akka/typed/GracefulStopDocSpec.scala index 3a096079b8..1577fda2ba 100644 --- a/akka-actor-typed-tests/src/test/scala/docs/akka/typed/GracefulStopDocSpec.scala +++ b/akka-actor-typed-tests/src/test/scala/docs/akka/typed/GracefulStopDocSpec.scala @@ -4,14 +4,11 @@ package docs.akka.typed //#imports +import akka.actor.typed.scaladsl.Behaviors +import akka.actor.typed.{ ActorSystem, Logger, PostStop } + import scala.concurrent.Await import scala.concurrent.duration._ - -import akka.actor.typed.ActorSystem -import akka.actor.typed.PostStop -import akka.actor.typed.scaladsl.Behaviors -import akka.event.LoggingAdapter - //#imports import akka.actor.typed.TypedAkkaSpecWithShutdown @@ -27,16 +24,16 @@ object GracefulStopDocSpec { final case object GracefulShutdown extends JobControlLanguage // Predefined cleanup operation - def cleanup(log: LoggingAdapter): Unit = log.info("Cleaning up!") + def cleanup(log: Logger): Unit = log.info("Cleaning up!") val mcpa = Behaviors.immutable[JobControlLanguage] { (ctx, msg) ⇒ msg match { case SpawnJob(jobName) ⇒ - ctx.system.log.info("Spawning job {}!", jobName) + ctx.log.info("Spawning job {}!", jobName) ctx.spawn(Job.job(jobName), name = jobName) Behaviors.same case GracefulShutdown ⇒ - ctx.system.log.info("Initiating graceful shutdown...") + ctx.log.info("Initiating graceful shutdown...") // perform graceful stop, executing cleanup before final system termination // behavior executing cleanup is passed as a parameter to Actor.stopped Behaviors.stopped { @@ -49,7 +46,7 @@ object GracefulStopDocSpec { } }.onSignal { case (ctx, PostStop) ⇒ - ctx.system.log.info("MCPA stopped") + ctx.log.info("MCPA stopped") Behaviors.same } } @@ -62,7 +59,7 @@ object GracefulStopDocSpec { def job(name: String) = Behaviors.onSignal[JobControlLanguage] { case (ctx, PostStop) ⇒ - ctx.system.log.info("Worker {} stopped", name) + ctx.log.info("Worker {} stopped", name) Behaviors.same } } diff --git a/akka-actor-typed/src/main/scala/akka/actor/typed/ActorRef.scala b/akka-actor-typed/src/main/scala/akka/actor/typed/ActorRef.scala index 95b302b449..70c9c71710 100644 --- a/akka-actor-typed/src/main/scala/akka/actor/typed/ActorRef.scala +++ b/akka-actor-typed/src/main/scala/akka/actor/typed/ActorRef.scala @@ -4,7 +4,6 @@ package akka.actor.typed import akka.annotation.InternalApi -import akka.event.typed.EventStream import akka.{ actor ⇒ a } import scala.annotation.unchecked.uncheckedVariance diff --git a/akka-actor-typed/src/main/scala/akka/actor/typed/ActorSystem.scala b/akka-actor-typed/src/main/scala/akka/actor/typed/ActorSystem.scala index 8ad87ff987..5b9918ddf8 100644 --- a/akka-actor-typed/src/main/scala/akka/actor/typed/ActorSystem.scala +++ b/akka-actor-typed/src/main/scala/akka/actor/typed/ActorSystem.scala @@ -19,7 +19,6 @@ import java.util.Optional import akka.actor.BootstrapSetup import akka.actor.typed.receptionist.Receptionist -import akka.event.typed.EventStream /** * An ActorSystem is home to a hierarchy of Actors. It is created using @@ -48,20 +47,12 @@ abstract class ActorSystem[-T] extends ActorRef[T] with Extensions { def logConfiguration(): Unit /** - * A reference to this system’s logFilter, which filters usage of the [[log]] - * [[akka.event.LoggingAdapter]] such that only permissible messages are sent - * via the [[eventStream]]. The default implementation will just test that - * the message is suitable for the current log level. - */ - def logFilter: e.LoggingFilter - - /** - * A [[akka.event.LoggingAdapter]] that can be used to emit log messages + * A [[akka.actor.typed.Logger]] that can be used to emit log messages * without specifying a more detailed source. Typically it is desirable to - * construct a dedicated LoggingAdapter within each Actor from that Actor’s - * [[ActorRef]] in order to identify the log messages. + * use the dedicated `Logger` available from each Actor’s [[ActorContext]] + * as that ties the log entries to the actor. */ - def log: e.LoggingAdapter + def log: Logger /** * Start-up time in milliseconds since the epoch. @@ -94,11 +85,6 @@ abstract class ActorSystem[-T] extends ActorRef[T] with Extensions { */ def scheduler: a.Scheduler - /** - * Main event bus of this actor system, used for example for logging. - */ - def eventStream: EventStream - /** * Facilities for lookup up thread-pools from configuration. */ diff --git a/akka-actor-typed/src/main/scala/akka/actor/typed/Logger.scala b/akka-actor-typed/src/main/scala/akka/actor/typed/Logger.scala new file mode 100644 index 0000000000..eaa212f47d --- /dev/null +++ b/akka-actor-typed/src/main/scala/akka/actor/typed/Logger.scala @@ -0,0 +1,559 @@ +/** + * Copyright (C) 2009-2018 Lightbend Inc. + */ +package akka.actor.typed + +import akka.annotation.{ DoNotInherit, InternalApi } + +/** + * A log marker is an additional metadata tag supported by some logging backends to identify "special" log events. + * In the Akka internal actors for example the "SECURITY" marker is used for warnings related to security. + * + * Not for user extension, create instances using factory methods + */ +@DoNotInherit +sealed trait LogMarker { + def name: String +} + +/** + * Factories for log markers + */ +object LogMarker { + + /** + * INTERNAL API + */ + @InternalApi + private final class LogMarkerImpl(name: String) extends akka.event.LogMarker(name) with LogMarker + + /** + * Scala API: Create a new log marker with the given name + */ + def apply(name: String): LogMarker = new LogMarkerImpl(name) + + /** + * Scala API: Create a new log marker with the given name + */ + def create(name: String): LogMarker = apply(name) + +} + +/** + * Logging API provided inside of actors through the actor context. + * + * All log-level methods support simple interpolation templates with up to four + * arguments placed by using {} within the template (first string + * argument): + * + * {{{ + * ctx.log.error(exception, "Exception while processing {} in state {}", msg, state) + * }}} + * + * More than four arguments can be defined by using an `Array` with the method with + * one argument parameter. + * + * *Rationale for an Akka-specific logging API:* + * Provided rather than a specific logging library logging API to not enforce a specific logging library on users but + * still providing a convenient, performant, asynchronous and testable logging solution. Additionally it allows unified + * logging for both user implemented actors and built in Akka actors where the actual logging backend can be selected + * by the user. This logging facade is also used by Akka internally, without having to depend on specific logging frameworks. + * + * The [[Logger]] of an actor is tied to the actor path and should not be shared with other threads outside of the actor. + * + * Not for user extension + */ +@DoNotInherit +abstract class Logger private[akka] () { + + /** + * Whether error logging is enabled on the actor system level, may not represent the setting all the way to the + * logger implementation, but when it does it allows avoiding unnecessary resource usage for log entries that + * will not actually end up in any logger output. + */ + def isErrorEnabled: Boolean + + /** + * Whether error logging is enabled on the actor system level, may not represent the setting all the way to the + * logger implementation, but when it does it allows avoiding unnecessary resource usage for log entries that + * will not actually end up in any logger output. + */ + def isWarningEnabled: Boolean + + /** + * Whether info logging is enabled on the actor system level, may not represent the setting all the way to the + * logger implementation, but when it does it allows avoiding unnecessary resource usage for log entries that + * will not actually end up in any logger output. + */ + def isInfoEnabled: Boolean + + /** + * Whether debug logging is enabled on the actor system level, may not represent the setting all the way to the + * logger implementation, but when it does it allows avoiding unnecessary resource usage for log entries that + * will not actually end up in any logger output. + */ + def isDebugEnabled: Boolean + + // message only error logging + + /** + * Log message at error level, without providing the exception that caused the error. + * + * @see [[Logger]] + */ + def error(message: String): Unit + /** + * Message template with 1 replacement argument. + * + * If `arg1` is an `Array` it will be expanded into replacement arguments, which is useful when + * there are more than four arguments. + * + * @see [[Logger]] + */ + def error(template: String, arg1: Any): Unit + /** + * Message template with 2 replacement arguments. + * + * @see [[Logger]] + */ + def error(template: String, arg1: Any, arg2: Any): Unit + /** + * Message template with 3 replacement arguments. + * + * @see [[Logger]] + */ + def error(template: String, arg1: Any, arg2: Any, arg3: Any): Unit + /** + * Message template with 4 replacement arguments. For more parameters see the single replacement version of this method. + * + * @see [[Logger]] + */ + def error(template: String, arg1: Any, arg2: Any, arg3: Any, arg4: Any): Unit + + // exception error logging + + /** + * Log message at error level, including the exception that caused the error. + * + * @see [[Logger]] + */ + def error(cause: Throwable, message: String): Unit + /** + * Message template with 1 replacement argument. + * + * If `arg1` is an `Array` it will be expanded into replacement arguments, which is useful when + * there are more than four arguments. + * + * @see [[Logger]] + */ + def error(cause: Throwable, template: String, arg1: Any): Unit + /** + * Message template with 2 replacement arguments. + * + * @see [[Logger]] + */ + def error(cause: Throwable, template: String, arg1: Any, arg2: Any): Unit + /** + * Message template with 3 replacement arguments. + * + * @see [[Logger]] + */ + def error(cause: Throwable, template: String, arg1: Any, arg2: Any, arg3: Any): Unit + /** + * Message template with 4 replacement arguments. For more parameters see the single replacement version of this method. + * + * @see [[Logger]] + */ + def error(cause: Throwable, template: String, arg1: Any, arg2: Any, arg3: Any, arg4: Any): Unit + + // marker error logging + + /** + * Log message at error level, including the exception that caused the error. + * + * @see [[Logger]] + */ + def error(marker: LogMarker, cause: Throwable, message: String): Unit + /** + * Message template with 1 replacement argument. + * + * If `arg1` is an `Array` it will be expanded into replacement arguments, which is useful when + * there are more than four arguments. + * + * @see [[Logger]] + */ + def error(marker: LogMarker, cause: Throwable, template: String, arg1: Any): Unit + /** + * Message template with 2 replacement arguments. + * + * @see [[Logger]] + */ + def error(marker: LogMarker, cause: Throwable, template: String, arg1: Any, arg2: Any): Unit + /** + * Message template with 3 replacement arguments. + * + * @see [[Logger]] + */ + def error(marker: LogMarker, cause: Throwable, template: String, arg1: Any, arg2: Any, arg3: Any): Unit + /** + * Message template with 4 replacement arguments. For more parameters see the single replacement version of this method. + * + * @see [[Logger]] + */ + def error(marker: LogMarker, cause: Throwable, template: String, arg1: Any, arg2: Any, arg3: Any, arg4: Any): Unit + /** + * Log message at error level, without providing the exception that caused the error. + * + * The marker argument can be picked up by various logging frameworks such as slf4j to mark this log statement as "special". + * + * @see [[Logger]] + */ + def error(marker: LogMarker, message: String): Unit + /** + * Message template with 1 replacement argument. + * + * The marker argument can be picked up by various logging frameworks such as slf4j to mark this log statement as "special". + * + * If `arg1` is an `Array` it will be expanded into replacement arguments, which is useful when + * there are more than four arguments. + * + * @see [[Logger]] + */ + def error(marker: LogMarker, template: String, arg1: Any): Unit + /** + * Message template with 2 replacement arguments. + * + * The marker argument can be picked up by various logging frameworks such as slf4j to mark this log statement as "special". + * + * @see [[Logger]] + */ + def error(marker: LogMarker, template: String, arg1: Any, arg2: Any): Unit + /** + * Message template with 3 replacement arguments. + * + * The marker argument can be picked up by various logging frameworks such as slf4j to mark this log statement as "special". + * + * @see [[Logger]] + */ + def error(marker: LogMarker, template: String, arg1: Any, arg2: Any, arg3: Any): Unit + /** + * Message template with 4 replacement arguments. For more parameters see the single replacement version of this method. + * + * The marker argument can be picked up by various logging frameworks such as slf4j to mark this log statement as "special". + * + * @see [[Logger]] + */ + def error(marker: LogMarker, template: String, arg1: Any, arg2: Any, arg3: Any, arg4: Any): Unit + + // message only warning logging + + /** + * Log message at warning level. + */ + def warning(message: String): Unit + /** + * Message template with 1 replacement argument. + * + * If `arg1` is an `Array` it will be expanded into replacement arguments, which is useful when + * there are more than four arguments. + * + * @see [[Logger]] + */ + def warning(template: String, arg1: Any): Unit + /** + * Message template with 2 replacement arguments. + * + * @see [[Logger]] + */ + def warning(template: String, arg1: Any, arg2: Any): Unit + /** + * Message template with 3 replacement arguments. + * + * @see [[Logger]] + */ + def warning(template: String, arg1: Any, arg2: Any, arg3: Any): Unit + /** + * Message template with 4 replacement arguments. For more parameters see the single replacement version of this method. + * + * @see [[Logger]] + */ + def warning(template: String, arg1: Any, arg2: Any, arg3: Any, arg4: Any): Unit + + /** + * Log message at warning level. + */ + def warning(cause: Throwable, message: String): Unit + /** + * Message template with 1 replacement argument. + * + * If `arg1` is an `Array` it will be expanded into replacement arguments, which is useful when + * there are more than four arguments. + * + * @see [[Logger]] + */ + def warning(cause: Throwable, template: String, arg1: Any): Unit + /** + * Message template with 2 replacement arguments. + * @see [[Logger]] + */ + def warning(cause: Throwable, template: String, arg1: Any, arg2: Any): Unit + /** + * Message template with 3 replacement arguments. + * @see [[Logger]] + */ + def warning(cause: Throwable, template: String, arg1: Any, arg2: Any, arg3: Any): Unit + /** + * Message template with 4 replacement arguments. For more parameters see the single replacement version of this method. + * @see [[Logger]] + */ + def warning(cause: Throwable, template: String, arg1: Any, arg2: Any, arg3: Any, arg4: Any): Unit + + // marker warning logging + /** + * Log message at warning level. + * + * The marker argument can be picked up by various logging frameworks such as slf4j to mark this log statement as "special". + */ + def warning(marker: LogMarker, message: String): Unit + /** + * Message template with 1 replacement argument. + * + * The marker argument can be picked up by various logging frameworks such as slf4j to mark this log statement as "special". + * + * If `arg1` is an `Array` it will be expanded into replacement arguments, which is useful when + * there are more than four arguments. + * + * @see [[Logger]] + */ + def warning(marker: LogMarker, template: String, arg1: Any): Unit + /** + * Message template with 2 replacement arguments. + * + * The marker argument can be picked up by various logging frameworks such as slf4j to mark this log statement as "special". + * + * @see [[Logger]] + */ + def warning(marker: LogMarker, template: String, arg1: Any, arg2: Any): Unit + /** + * Message template with 3 replacement arguments. + * + * The marker argument can be picked up by various logging frameworks such as slf4j to mark this log statement as "special". + * + * @see [[Logger]] + */ + def warning(marker: LogMarker, template: String, arg1: Any, arg2: Any, arg3: Any): Unit + /** + * Message template with 4 replacement arguments. For more parameters see the single replacement version of this method. + * + * @see [[Logger]] + */ + def warning(marker: LogMarker, template: String, arg1: Any, arg2: Any, arg3: Any, arg4: Any): Unit + + /** + * Log message at warning level. + * + * The marker argument can be picked up by various logging frameworks such as slf4j to mark this log statement as "special". + * @see [[Logger]] + */ + def warning(marker: LogMarker, cause: Throwable, message: String): Unit + /** + * Message template with 1 replacement argument. + * + * The marker argument can be picked up by various logging frameworks such as slf4j to mark this log statement as "special". + * + * If `arg1` is an `Array` it will be expanded into replacement arguments, which is useful when + * there are more than four arguments. + * + * @see [[Logger]] + */ + def warning(marker: LogMarker, cause: Throwable, template: String, arg1: Any): Unit + /** + * Message template with 2 replacement arguments. + * + * The marker argument can be picked up by various logging frameworks such as slf4j to mark this log statement as "special". + * + * @see [[Logger]] + */ + def warning(marker: LogMarker, cause: Throwable, template: String, arg1: Any, arg2: Any): Unit + /** + * Message template with 3 replacement arguments. + * + * The marker argument can be picked up by various logging frameworks such as slf4j to mark this log statement as "special". + * + * @see [[Logger]] + */ + def warning(marker: LogMarker, cause: Throwable, template: String, arg1: Any, arg2: Any, arg3: Any): Unit + /** + * Message template with 4 replacement arguments. For more parameters see the single replacement version of this method. + * + * The marker argument can be picked up by various logging frameworks such as slf4j to mark this log statement as "special". + * + * @see [[Logger]] + */ + def warning(marker: LogMarker, cause: Throwable, template: String, arg1: Any, arg2: Any, arg3: Any, arg4: Any): Unit + + // message only info logging + + /** + * Log message at info level. + * + * @see [[Logger]] + */ + def info(message: String): Unit + /** + * Message template with 1 replacement argument. + * + * If `arg1` is an `Array` it will be expanded into replacement arguments, which is useful when + * there are more than four arguments. + * + * @see [[Logger]] + */ + def info(template: String, arg1: Any): Unit + /** + * Message template with 2 replacement arguments. + * + * @see [[Logger]] + */ + def info(template: String, arg1: Any, arg2: Any): Unit + /** + * Message template with 3 replacement arguments. + * + * @see [[Logger]] + */ + def info(template: String, arg1: Any, arg2: Any, arg3: Any): Unit + /** + * Message template with 4 replacement arguments. For more parameters see the single replacement version of this method. + * + * @see [[Logger]] + */ + def info(template: String, arg1: Any, arg2: Any, arg3: Any, arg4: Any): Unit + + // marker info logging + + /** + * Log message at info level. + * + * The marker argument can be picked up by various logging frameworks such as slf4j to mark this log statement as "special". + * + * @see [[Logger]] + */ + def info(marker: LogMarker, message: String): Unit + /** + * Message template with 1 replacement argument. + * + * The marker argument can be picked up by various logging frameworks such as slf4j to mark this log statement as "special". + * + * If `arg1` is an `Array` it will be expanded into replacement arguments, which is useful when + * there are more than four arguments. + * + * @see [[Logger]] + */ + def info(marker: LogMarker, template: String, arg1: Any): Unit + /** + * Message template with 2 replacement arguments. + * + * The marker argument can be picked up by various logging frameworks such as slf4j to mark this log statement as "special". + * + * @see [[Logger]] + */ + def info(marker: LogMarker, template: String, arg1: Any, arg2: Any): Unit + /** + * Message template with 3 replacement arguments. + * + * The marker argument can be picked up by various logging frameworks such as slf4j to mark this log statement as "special". + * + * @see [[Logger]] + */ + def info(marker: LogMarker, template: String, arg1: Any, arg2: Any, arg3: Any): Unit + /** + * Message template with 4 replacement arguments. For more parameters see the single replacement version of this method. + * + * The marker argument can be picked up by various logging frameworks such as slf4j to mark this log statement as "special". + * + * @see [[Logger]] + */ + def info(marker: LogMarker, template: String, arg1: Any, arg2: Any, arg3: Any, arg4: Any): Unit + + // message only debug logging + + /** + * Log message at debug level. + * + * @see [[Logger]] + */ + def debug(message: String): Unit + /** + * Message template with 1 replacement argument. + * + * If `arg1` is an `Array` it will be expanded into replacement arguments, which is useful when + * there are more than four arguments. + * + * @see [[Logger]] + */ + def debug(template: String, arg1: Any): Unit + /** + * Message template with 2 replacement arguments. + * + * @see [[Logger]] + */ + def debug(template: String, arg1: Any, arg2: Any): Unit + /** + * Message template with 3 replacement arguments. + * + * @see [[Logger]] + */ + def debug(template: String, arg1: Any, arg2: Any, arg3: Any): Unit + /** + * Message template with 4 replacement arguments. For more parameters see the single replacement version of this method. + * + * @see [[Logger]] + */ + def debug(template: String, arg1: Any, arg2: Any, arg3: Any, arg4: Any): Unit + + // marker debug logging + + /** + * Log message at debug level. + * + * The marker argument can be picked up by various logging frameworks such as slf4j to mark this log statement as "special". + * + * @see [[Logger]] + */ + def debug(marker: LogMarker, message: String): Unit + /** + * Message template with 1 replacement argument. + * + * The marker argument can be picked up by various logging frameworks such as slf4j to mark this log statement as "special". + * + * If `arg1` is an `Array` it will be expanded into replacement arguments, which is useful when + * there are more than four arguments. + * + * @see [[Logger]] + */ + def debug(marker: LogMarker, template: String, arg1: Any): Unit + /** + * Message template with 2 replacement arguments. + * + * The marker argument can be picked up by various logging frameworks such as slf4j to mark this log statement as "special". + * + * @see [[Logger]] + */ + def debug(marker: LogMarker, template: String, arg1: Any, arg2: Any): Unit + /** + * Message template with 3 replacement arguments. + * + * The marker argument can be picked up by various logging frameworks such as slf4j to mark this log statement as "special". + * + * @see [[Logger]] + */ + def debug(marker: LogMarker, template: String, arg1: Any, arg2: Any, arg3: Any): Unit + /** + * Message template with 4 replacement arguments. For more parameters see the single replacement version of this method. + * + * The marker argument can be picked up by various logging frameworks such as slf4j to mark this log statement as "special". + * + * @see [[Logger]] + */ + def debug(marker: LogMarker, template: String, arg1: Any, arg2: Any, arg3: Any, arg4: Any): Unit + +} diff --git a/akka-actor-typed/src/main/scala/akka/actor/typed/internal/ActorContextImpl.scala b/akka-actor-typed/src/main/scala/akka/actor/typed/internal/ActorContextImpl.scala index dcef4a0185..be77022977 100644 --- a/akka-actor-typed/src/main/scala/akka/actor/typed/internal/ActorContextImpl.scala +++ b/akka-actor-typed/src/main/scala/akka/actor/typed/internal/ActorContextImpl.scala @@ -18,6 +18,7 @@ import scala.util.Try import akka.annotation.InternalApi import akka.util.OptionVal +import akka.event.LoggingAdapter import akka.util.Timeout /** @@ -58,6 +59,8 @@ import akka.util.Timeout override def getSystem: akka.actor.typed.ActorSystem[Void] = system.asInstanceOf[ActorSystem[Void]] + override def getLog: Logger = log + override def spawn[U](behavior: akka.actor.typed.Behavior[U], name: String): akka.actor.typed.ActorRef[U] = spawn(behavior, name, Props.empty) diff --git a/akka-actor-typed/src/main/scala/akka/actor/typed/internal/LoggingBehaviorImpl.scala b/akka-actor-typed/src/main/scala/akka/actor/typed/internal/LoggingBehaviorImpl.scala new file mode 100644 index 0000000000..b35ea3922b --- /dev/null +++ b/akka-actor-typed/src/main/scala/akka/actor/typed/internal/LoggingBehaviorImpl.scala @@ -0,0 +1,43 @@ +/** + * Copyright (C) 2009-2018 Lightbend Inc. + */ +package akka.actor.typed.internal + +import akka.actor.typed.Behavior +import akka.actor.typed.internal.adapter.LoggerAdapterImpl +import akka.annotation.InternalApi + +import scala.reflect.ClassTag + +/** + * INTERNAL API + * + * Regular logging is supported directly on the ActorContext, but more advanced logging needs are covered here + */ +@InternalApi object LoggingBehaviorImpl { + + /** + * MDC logging support + * + * @param mdcForMessage Is invoked before each message is handled, allowing to setup MDC, MDC is cleared after + * each message processing by the inner behavior is done. + * @param behavior The actual behavior handling the messages, the MDC is used for the log entries logged through + * `ActorContext.log` + */ + def withMdc[T: ClassTag](mdcForMessage: T ⇒ Map[String, Any], behavior: Behavior[T]): Behavior[T] = + BehaviorImpl.intercept[T, T]( + beforeMessage = { (ctx, msg) ⇒ + ctx.log.asInstanceOf[LoggerAdapterImpl].mdc = mdcForMessage(msg) + msg + }, + beforeSignal = (_, _) ⇒ true, + afterMessage = { (ctx, _, behavior) ⇒ + ctx.log.asInstanceOf[LoggerAdapterImpl].mdc = Map.empty + behavior + }, + afterSignal = (_, _, behavior) ⇒ behavior, + behavior = behavior, + toStringPrefix = "withMdc" + ) + +} diff --git a/akka-actor-typed/src/main/scala/akka/actor/typed/internal/Restarter.scala b/akka-actor-typed/src/main/scala/akka/actor/typed/internal/Restarter.scala index 9102dd1c8c..6497bb6d47 100644 --- a/akka-actor-typed/src/main/scala/akka/actor/typed/internal/Restarter.scala +++ b/akka-actor-typed/src/main/scala/akka/actor/typed/internal/Restarter.scala @@ -70,8 +70,7 @@ import akka.actor.typed.scaladsl.Behaviors protected def restart(ctx: ActorContext[T], initialBehavior: Behavior[T], startedBehavior: Behavior[T]): Supervisor[T, Thr] = { try Behavior.interpretSignal(startedBehavior, ctx, PreRestart) catch { - case NonFatal(ex) ⇒ publish(ctx, Logging.Error(ex, ctx.asScala.self.path.toString, behavior.getClass, - "failure during PreRestart")) + case NonFatal(ex) ⇒ ctx.asScala.log.error(ex, "failure during PreRestart") } // no need to canonicalize, it's done in the calling methods wrap(Supervisor.initialUndefer(ctx, initialBehavior), afterException = true) @@ -96,11 +95,8 @@ import akka.actor.typed.scaladsl.Behaviors protected def log(ctx: ActorContext[T], ex: Thr): Unit = { if (loggingEnabled) - publish(ctx, Logging.Error(ex, ctx.asScala.self.toString, behavior.getClass, ex.getMessage)) + ctx.asScala.log.error(ex, ex.getMessage) } - - protected final def publish(ctx: ActorContext[T], e: Logging.LogEvent): Unit = - try ctx.asScala.system.eventStream.publish(e) catch { case NonFatal(_) ⇒ } } /** @@ -228,7 +224,8 @@ import akka.actor.typed.scaladsl.Behaviors override def receiveSignal(ctx: ActorContext[Any], signal: Signal): Behavior[Any] = { if (blackhole) { - ctx.asScala.system.eventStream.publish(Dropped(signal, ctx.asScala.self)) + import scaladsl.adapter._ + ctx.asScala.system.toUntyped.eventStream.publish(Dropped(signal, ctx.asScala.self)) Behavior.same } else super.receiveSignal(ctx, signal) @@ -249,7 +246,8 @@ import akka.actor.typed.scaladsl.Behaviors Behavior.same case _ ⇒ if (blackhole) { - ctx.asScala.system.eventStream.publish(Dropped(msg, ctx.asScala.self)) + import scaladsl.adapter._ + ctx.asScala.system.toUntyped.eventStream.publish(Dropped(msg, ctx.asScala.self)) Behavior.same } else super.receiveMessage(ctx, msg) @@ -261,8 +259,7 @@ import akka.actor.typed.scaladsl.Behaviors log(ctx, ex) // actual restart happens after the scheduled backoff delay try Behavior.interpretSignal(behavior, ctx, PreRestart) catch { - case NonFatal(ex2) ⇒ publish(ctx, Logging.Error(ex2, ctx.asScala.self.path.toString, behavior.getClass, - "failure during PreRestart")) + case NonFatal(ex2) ⇒ ctx.asScala.log.error(ex2, "failure during PreRestart") } val restartDelay = calculateDelay(restartCount, strategy.minBackoff, strategy.maxBackoff, strategy.randomFactor) ctx.asScala.schedule(restartDelay, ctx.asScala.self, ScheduledRestart) diff --git a/akka-actor-typed/src/main/scala/akka/actor/typed/internal/TimerSchedulerImpl.scala b/akka-actor-typed/src/main/scala/akka/actor/typed/internal/TimerSchedulerImpl.scala index f9922be4ca..5de314eaf0 100644 --- a/akka-actor-typed/src/main/scala/akka/actor/typed/internal/TimerSchedulerImpl.scala +++ b/akka-actor-typed/src/main/scala/akka/actor/typed/internal/TimerSchedulerImpl.scala @@ -41,8 +41,6 @@ import scala.reflect.ClassTag extends scaladsl.TimerScheduler[T] with javadsl.TimerScheduler[T] { import TimerSchedulerImpl._ - // FIXME change to a class specific logger, see issue #21219 - private val log = ctx.system.log private var timers: Map[Any, Timer[T]] = Map.empty private val timerGen = Iterator from 1 @@ -71,7 +69,7 @@ import scala.reflect.ClassTag }(ExecutionContexts.sameThreadExecutionContext) val nextTimer = Timer(key, msg, repeat, nextGen, task) - log.debug("Start timer [{}] with generation [{}]", key, nextGen) + ctx.log.debug("Start timer [{}] with generation [{}]", key, nextGen) timers = timers.updated(key, nextTimer) } @@ -86,13 +84,13 @@ import scala.reflect.ClassTag } private def cancelTimer(timer: Timer[T]): Unit = { - log.debug("Cancel timer [{}] with generation [{}]", timer.key, timer.generation) + ctx.log.debug("Cancel timer [{}] with generation [{}]", timer.key, timer.generation) timer.task.cancel() timers -= timer.key } override def cancelAll(): Unit = { - log.debug("Cancel all timers") + ctx.log.debug("Cancel all timers") timers.valuesIterator.foreach { timer ⇒ timer.task.cancel() } @@ -103,12 +101,12 @@ import scala.reflect.ClassTag timers.get(timerMsg.key) match { case None ⇒ // it was from canceled timer that was already enqueued in mailbox - log.debug("Received timer [{}] that has been removed, discarding", timerMsg.key) + ctx.log.debug("Received timer [{}] that has been removed, discarding", timerMsg.key) null.asInstanceOf[T] // message should be ignored case Some(t) ⇒ if (timerMsg.owner ne this) { // after restart, it was from an old instance that was enqueued in mailbox before canceled - log.debug("Received timer [{}] from old restarted instance, discarding", timerMsg.key) + ctx.log.debug("Received timer [{}] from old restarted instance, discarding", timerMsg.key) null.asInstanceOf[T] // message should be ignored } else if (timerMsg.generation == t.generation) { // valid timer @@ -117,7 +115,7 @@ import scala.reflect.ClassTag t.msg } else { // it was from an old timer that was enqueued in mailbox before canceled - log.debug( + ctx.log.debug( "Received timer [{}] from from old generation [{}], expected generation [{}], discarding", timerMsg.key, timerMsg.generation, t.generation) null.asInstanceOf[T] // message should be ignored diff --git a/akka-actor-typed/src/main/scala/akka/actor/typed/internal/adapter/ActorContextAdapter.scala b/akka-actor-typed/src/main/scala/akka/actor/typed/internal/adapter/ActorContextAdapter.scala index 0289dce243..ca74f815ea 100644 --- a/akka-actor-typed/src/main/scala/akka/actor/typed/internal/adapter/ActorContextAdapter.scala +++ b/akka-actor-typed/src/main/scala/akka/actor/typed/internal/adapter/ActorContextAdapter.scala @@ -5,20 +5,25 @@ package akka.actor.typed package internal package adapter +import akka.actor.ExtendedActorSystem +import akka.actor.typed.Behavior.UntypedBehavior +import akka.annotation.InternalApi +import akka.util.OptionVal import akka.{ ConfigurationException, actor ⇒ a } -import scala.concurrent.duration._ import scala.concurrent.ExecutionContextExecutor -import akka.annotation.InternalApi -import akka.actor.typed.Behavior.UntypedBehavior +import scala.concurrent.duration._ /** * INTERNAL API. Wrapping an [[akka.actor.ActorContext]] as an [[ActorContext]]. */ -@InternalApi private[akka] class ActorContextAdapter[T](val untyped: a.ActorContext) extends ActorContextImpl[T] { +@InternalApi private[akka] final class ActorContextAdapter[T](val untyped: a.ActorContext) extends ActorContextImpl[T] { import ActorRefAdapter.toUntyped + // lazily initialized + private var actorLogger: OptionVal[Logger] = OptionVal.None + override def self = ActorRefAdapter(untyped.self) override val system = ActorSystemAdapter(untyped.system) override def mailboxCapacity = 1 << 29 // FIXME @@ -65,6 +70,20 @@ import akka.actor.typed.Behavior.UntypedBehavior val ref = cell.addFunctionRef((_, msg) ⇒ untyped.self ! AdaptMessage[U, T](msg.asInstanceOf[U], f), _name) ActorRefAdapter[U](ref) } + + override def log: Logger = { + actorLogger match { + case OptionVal.Some(logger) ⇒ logger + case OptionVal.None ⇒ + import scala.language.existentials + val logSource = self.path.toString + val logClass = classOf[Behavior[_]] // FIXME figure out a better class somehow + val system = untyped.system.asInstanceOf[ExtendedActorSystem] + val logger = new LoggerAdapterImpl(system.eventStream, logClass, logSource, system.logFilter) + actorLogger = OptionVal.Some(logger) + logger + } + } } /** diff --git a/akka-actor-typed/src/main/scala/akka/actor/typed/internal/adapter/ActorSystemAdapter.scala b/akka-actor-typed/src/main/scala/akka/actor/typed/internal/adapter/ActorSystemAdapter.scala index 0a4bdade88..6fad8acf39 100644 --- a/akka-actor-typed/src/main/scala/akka/actor/typed/internal/adapter/ActorSystemAdapter.scala +++ b/akka-actor-typed/src/main/scala/akka/actor/typed/internal/adapter/ActorSystemAdapter.scala @@ -17,7 +17,6 @@ import akka.util.Timeout import scala.concurrent.Future import akka.annotation.InternalApi -import akka.event.typed.EventStream import scala.compat.java8.FutureConverters @@ -57,11 +56,9 @@ import scala.compat.java8.FutureConverters override def shutdown(): Unit = () // there was no shutdown in untyped Akka } override def dynamicAccess: a.DynamicAccess = untyped.dynamicAccess - override def eventStream: EventStream = new EventStreamAdapter(untyped.eventStream) implicit override def executionContext: scala.concurrent.ExecutionContextExecutor = untyped.dispatcher - override def log: akka.event.LoggingAdapter = untyped.log + override val log: Logger = new LoggerAdapterImpl(untyped.eventStream, getClass, name, untyped.logFilter) override def logConfiguration(): Unit = untyped.logConfiguration() - override def logFilter: akka.event.LoggingFilter = untyped.logFilter override def name: String = untyped.name override def scheduler: akka.actor.Scheduler = untyped.scheduler override def settings: Settings = new Settings(untyped.settings) diff --git a/akka-actor-typed/src/main/scala/akka/actor/typed/internal/adapter/EventStreamAdapter.scala b/akka-actor-typed/src/main/scala/akka/actor/typed/internal/adapter/EventStreamAdapter.scala deleted file mode 100644 index e66cf16955..0000000000 --- a/akka-actor-typed/src/main/scala/akka/actor/typed/internal/adapter/EventStreamAdapter.scala +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Copyright (C) 2016-2018 Lightbend Inc. - */ -package akka.actor.typed -package internal -package adapter - -import akka.{ event ⇒ e } -import akka.annotation.InternalApi -import akka.event.typed.EventStream - -/** - * INTERNAL API - */ -@InternalApi private[typed] class EventStreamAdapter(untyped: e.EventStream) extends EventStream { - def logLevel: e.Logging.LogLevel = untyped.logLevel - - def publish[T](event: T): Unit = untyped.publish(event.asInstanceOf[AnyRef]) - - def setLogLevel(loglevel: e.Logging.LogLevel): Unit = untyped.setLogLevel(loglevel) - - def subscribe[T](subscriber: ActorRef[T], to: Class[T]): Boolean = - subscriber match { - case adapter: ActorRefAdapter[_] ⇒ untyped.subscribe(adapter.untyped, to) - case _ ⇒ - throw new UnsupportedOperationException("Cannot subscribe native typed ActorRef") - } - - def unsubscribe[T](subscriber: ActorRef[T]): Unit = - subscriber match { - case adapter: ActorRefAdapter[_] ⇒ untyped.unsubscribe(adapter.untyped) - case _ ⇒ - throw new UnsupportedOperationException("Cannot unsubscribe native typed ActorRef") - } - - def unsubscribe[T](subscriber: ActorRef[T], from: Class[T]): Boolean = - subscriber match { - case adapter: ActorRefAdapter[_] ⇒ untyped.unsubscribe(adapter.untyped, from) - case _ ⇒ - throw new UnsupportedOperationException("Cannot unsubscribe native typed ActorRef") - } - -} diff --git a/akka-actor-typed/src/main/scala/akka/actor/typed/internal/adapter/LoggerAdapterImpl.scala b/akka-actor-typed/src/main/scala/akka/actor/typed/internal/adapter/LoggerAdapterImpl.scala new file mode 100644 index 0000000000..fb61cea692 --- /dev/null +++ b/akka-actor-typed/src/main/scala/akka/actor/typed/internal/adapter/LoggerAdapterImpl.scala @@ -0,0 +1,347 @@ +/** + * Copyright (C) 2009-2018 Lightbend Inc. + */ +package akka.actor.typed.internal.adapter + +import akka.actor.typed.{ LogMarker, Logger } +import akka.annotation.InternalApi +import akka.event.Logging.{ Debug, Error, Info, Warning } +import akka.event.{ LoggingBus, LoggingFilter, LogMarker ⇒ UntypedLM } +import akka.util.OptionVal + +/** + * INTERNAL API + */ +@InternalApi +private[akka] class LoggerAdapterImpl(bus: LoggingBus, logClass: Class[_], logSource: String, loggingFilter: LoggingFilter) extends Logger { + + private[akka] var mdc: Map[String, Any] = Map.empty + + override def isErrorEnabled = loggingFilter.isErrorEnabled(logClass, logSource) + override def isWarningEnabled = loggingFilter.isWarningEnabled(logClass, logSource) + override def isInfoEnabled = loggingFilter.isInfoEnabled(logClass, logSource) + override def isDebugEnabled = loggingFilter.isDebugEnabled(logClass, logSource) + + override def error(message: String): Unit = { + if (isErrorEnabled) notifyError(message, OptionVal.None, OptionVal.None) + } + + override def error(template: String, arg1: Any): Unit = { + if (isErrorEnabled) notifyError(format(template, arg1), OptionVal.None, OptionVal.None) + } + + override def error(template: String, arg1: Any, arg2: Any): Unit = { + if (isErrorEnabled) notifyError(format(template, arg1, arg2), OptionVal.None, OptionVal.None) + } + + override def error(template: String, arg1: Any, arg2: Any, arg3: Any): Unit = { + if (isErrorEnabled) notifyError(format(template, arg1, arg2, arg3), OptionVal.None, OptionVal.None) + } + + override def error(template: String, arg1: Any, arg2: Any, arg3: Any, arg4: Any): Unit = { + if (isErrorEnabled) notifyError(format(template, arg1, arg2, arg3, arg4), OptionVal.None, OptionVal.None) + } + + override def error(cause: Throwable, message: String): Unit = { + if (isErrorEnabled) notifyError(message, OptionVal.Some(cause), OptionVal.None) + } + + override def error(cause: Throwable, template: String, arg1: Any): Unit = { + if (isErrorEnabled) notifyError(format(template, arg1), OptionVal.Some(cause), OptionVal.None) + } + + override def error(cause: Throwable, template: String, arg1: Any, arg2: Any): Unit = { + if (isErrorEnabled) notifyError(format(template, arg1, arg2), OptionVal.Some(cause), OptionVal.None) + } + + override def error(cause: Throwable, template: String, arg1: Any, arg2: Any, arg3: Any): Unit = { + if (isErrorEnabled) notifyError(format(template, arg1, arg2, arg3), OptionVal.Some(cause), OptionVal.None) + } + + override def error(cause: Throwable, template: String, arg1: Any, arg2: Any, arg3: Any, arg4: Any): Unit = { + if (isErrorEnabled) notifyError(format(template, arg1, arg2, arg3, arg4), OptionVal.Some(cause), OptionVal.None) + } + + override def error(marker: LogMarker, cause: Throwable, message: String): Unit = { + if (isErrorEnabled) notifyError(message, OptionVal.Some(cause), OptionVal.Some(marker)) + } + + override def error(marker: LogMarker, cause: Throwable, template: String, arg1: Any): Unit = { + if (isErrorEnabled) notifyError(format(template, arg1), OptionVal.Some(cause), OptionVal.Some(marker)) + } + + override def error(marker: LogMarker, cause: Throwable, template: String, arg1: Any, arg2: Any): Unit = { + if (isErrorEnabled) notifyError(format(template, arg1, arg2), OptionVal.Some(cause), OptionVal.Some(marker)) + } + + override def error(marker: LogMarker, cause: Throwable, template: String, arg1: Any, arg2: Any, arg3: Any): Unit = { + if (isErrorEnabled) notifyError(format(template, arg1, arg2, arg3), OptionVal.Some(cause), OptionVal.Some(marker)) + } + + override def error(marker: LogMarker, cause: Throwable, template: String, arg1: Any, arg2: Any, arg3: Any, arg4: Any): Unit = { + if (isErrorEnabled) notifyError(format(template, arg1, arg2, arg3, arg4), OptionVal.Some(cause), OptionVal.Some(marker)) + } + + override def error(marker: LogMarker, message: String): Unit = { + if (isErrorEnabled) notifyError(message, OptionVal.None, OptionVal.Some(marker)) + } + + override def error(marker: LogMarker, template: String, arg1: Any): Unit = { + if (isErrorEnabled) notifyError(format(template, arg1), OptionVal.None, OptionVal.Some(marker)) + } + + override def error(marker: LogMarker, template: String, arg1: Any, arg2: Any): Unit = { + if (isErrorEnabled) notifyError(format(template, arg1, arg2), OptionVal.None, OptionVal.Some(marker)) + } + + override def error(marker: LogMarker, template: String, arg1: Any, arg2: Any, arg3: Any): Unit = { + if (isErrorEnabled) notifyError(format(template, arg1, arg2, arg3), OptionVal.None, OptionVal.Some(marker)) + } + + override def error(marker: LogMarker, template: String, arg1: Any, arg2: Any, arg3: Any, arg4: Any): Unit = { + if (isErrorEnabled) notifyError(format(template, arg1, arg2, arg3, arg4), OptionVal.None, OptionVal.Some(marker)) + } + + override def warning(message: String): Unit = { + if (isWarningEnabled) notifyWarning(message, OptionVal.None, OptionVal.None) + } + + override def warning(template: String, arg1: Any): Unit = { + if (isWarningEnabled) notifyWarning(format(template, arg1), OptionVal.None, OptionVal.None) + } + + override def warning(template: String, arg1: Any, arg2: Any): Unit = { + if (isWarningEnabled) notifyWarning(format(template, arg1, arg2), OptionVal.None, OptionVal.None) + } + + override def warning(template: String, arg1: Any, arg2: Any, arg3: Any): Unit = { + if (isWarningEnabled) notifyWarning(format(template, arg1, arg2, arg3), OptionVal.None, OptionVal.None) + } + + override def warning(template: String, arg1: Any, arg2: Any, arg3: Any, arg4: Any): Unit = { + if (isWarningEnabled) notifyWarning(format(template, arg1, arg2, arg3, arg4), OptionVal.None, OptionVal.None) + } + + override def warning(cause: Throwable, message: String): Unit = { + if (isWarningEnabled) notifyWarning(message, OptionVal.None, OptionVal.Some(cause)) + } + + override def warning(cause: Throwable, template: String, arg1: Any): Unit = { + if (isWarningEnabled) notifyWarning(format(template, arg1), OptionVal.None, OptionVal.Some(cause)) + } + + override def warning(cause: Throwable, template: String, arg1: Any, arg2: Any): Unit = { + if (isWarningEnabled) notifyWarning(format(template, arg1, arg2), OptionVal.None, OptionVal.Some(cause)) + } + + override def warning(cause: Throwable, template: String, arg1: Any, arg2: Any, arg3: Any): Unit = { + if (isWarningEnabled) notifyWarning(format(template, arg1, arg2, arg3), OptionVal.None, OptionVal.Some(cause)) + } + + override def warning(cause: Throwable, template: String, arg1: Any, arg2: Any, arg3: Any, arg4: Any): Unit = { + if (isWarningEnabled) notifyWarning(format(template, arg1, arg2, arg3, arg4), OptionVal.None, OptionVal.Some(cause)) + } + + override def warning(marker: LogMarker, cause: Throwable, template: String, arg1: Any): Unit = { + if (isWarningEnabled) notifyWarning(format(template, arg1), OptionVal.Some(marker), OptionVal.Some(cause)) + } + + override def warning(marker: LogMarker, cause: Throwable, template: String, arg1: Any, arg2: Any): Unit = { + if (isWarningEnabled) notifyWarning(format(template, arg1, arg2), OptionVal.Some(marker), OptionVal.Some(cause)) + } + + override def warning(marker: LogMarker, cause: Throwable, template: String, arg1: Any, arg2: Any, arg3: Any): Unit = { + if (isWarningEnabled) notifyWarning(format(template, arg1, arg2, arg3), OptionVal.Some(marker), OptionVal.Some(cause)) + } + + override def warning(marker: LogMarker, cause: Throwable, template: String, arg1: Any, arg2: Any, arg3: Any, arg4: Any): Unit = { + if (isWarningEnabled) notifyWarning(format(template, arg1, arg2, arg3, arg4), OptionVal.Some(marker), OptionVal.Some(cause)) + } + + override def warning(marker: LogMarker, cause: Throwable, message: String): Unit = { + if (isWarningEnabled) notifyWarning(message, OptionVal.Some(marker), OptionVal.Some(cause)) + } + + override def warning(marker: LogMarker, message: String): Unit = { + if (isWarningEnabled) notifyWarning(message, OptionVal.Some(marker), OptionVal.None) + } + + override def warning(marker: LogMarker, template: String, arg1: Any): Unit = { + if (isWarningEnabled) notifyWarning(format(template, arg1), OptionVal.Some(marker), OptionVal.None) + } + + override def warning(marker: LogMarker, template: String, arg1: Any, arg2: Any): Unit = { + if (isWarningEnabled) notifyWarning(format(template, arg1, arg2), OptionVal.Some(marker), OptionVal.None) + } + + override def warning(marker: LogMarker, template: String, arg1: Any, arg2: Any, arg3: Any): Unit = { + if (isWarningEnabled) notifyWarning(format(template, arg1, arg2, arg3), OptionVal.Some(marker), OptionVal.None) + } + + override def warning(marker: LogMarker, template: String, arg1: Any, arg2: Any, arg3: Any, arg4: Any): Unit = { + if (isWarningEnabled) notifyWarning(format(template, arg1, arg2, arg3, arg4), OptionVal.Some(marker), OptionVal.None) + } + + override def info(message: String): Unit = { + if (isInfoEnabled) notifyInfo(message, OptionVal.None) + } + + override def info(template: String, arg1: Any): Unit = { + if (isInfoEnabled) notifyInfo(format(template, arg1), OptionVal.None) + } + + override def info(template: String, arg1: Any, arg2: Any): Unit = { + if (isInfoEnabled) notifyInfo(format(template, arg1, arg2), OptionVal.None) + } + + override def info(template: String, arg1: Any, arg2: Any, arg3: Any): Unit = { + if (isInfoEnabled) notifyInfo(format(template, arg1, arg2, arg3), OptionVal.None) + } + + override def info(template: String, arg1: Any, arg2: Any, arg3: Any, arg4: Any): Unit = { + if (isInfoEnabled) notifyInfo(format(template, arg1, arg2, arg3, arg4), OptionVal.None) + } + + override def info(marker: LogMarker, message: String): Unit = { + if (isInfoEnabled) notifyInfo(message, OptionVal.Some(marker)) + } + + override def info(marker: LogMarker, template: String, arg1: Any): Unit = { + if (isInfoEnabled) notifyInfo(format(template, arg1), OptionVal.Some(marker)) + } + + override def info(marker: LogMarker, template: String, arg1: Any, arg2: Any): Unit = { + if (isInfoEnabled) notifyInfo(format(template, arg1, arg2), OptionVal.Some(marker)) + } + + override def info(marker: LogMarker, template: String, arg1: Any, arg2: Any, arg3: Any): Unit = { + if (isInfoEnabled) notifyInfo(format(template, arg1, arg2, arg3), OptionVal.Some(marker)) + } + + override def info(marker: LogMarker, template: String, arg1: Any, arg2: Any, arg3: Any, arg4: Any): Unit = { + if (isInfoEnabled) notifyInfo(format(template, arg1, arg2, arg3, arg4), OptionVal.Some(marker)) + } + + override def debug(message: String): Unit = { + if (isDebugEnabled) notifyDebug(message, OptionVal.None) + } + + override def debug(template: String, arg1: Any): Unit = { + if (isDebugEnabled) notifyDebug(format(template, arg1), OptionVal.None) + } + + override def debug(template: String, arg1: Any, arg2: Any): Unit = { + if (isDebugEnabled) notifyDebug(format(template, arg1, arg2), OptionVal.None) + } + + override def debug(template: String, arg1: Any, arg2: Any, arg3: Any): Unit = { + if (isDebugEnabled) notifyDebug(format(template, arg1, arg2, arg3), OptionVal.None) + } + + override def debug(template: String, arg1: Any, arg2: Any, arg3: Any, arg4: Any): Unit = { + if (isDebugEnabled) notifyDebug(format(template, arg1, arg2, arg3, arg4), OptionVal.None) + } + + override def debug(marker: LogMarker, message: String): Unit = { + if (isDebugEnabled) notifyDebug(message, OptionVal.Some(marker)) + } + + override def debug(marker: LogMarker, template: String, arg1: Any): Unit = { + if (isDebugEnabled) notifyDebug(format(template, arg1), OptionVal.Some(marker)) + } + + override def debug(marker: LogMarker, template: String, arg1: Any, arg2: Any): Unit = { + if (isDebugEnabled) notifyDebug(format(template, arg1, arg2), OptionVal.Some(marker)) + } + + override def debug(marker: LogMarker, template: String, arg1: Any, arg2: Any, arg3: Any): Unit = { + if (isDebugEnabled) notifyDebug(format(template, arg1, arg2, arg3), OptionVal.Some(marker)) + } + + override def debug(marker: LogMarker, template: String, arg1: Any, arg2: Any, arg3: Any, arg4: Any): Unit = { + if (isDebugEnabled) notifyDebug(format(template, arg1, arg2, arg3, arg4), OptionVal.Some(marker)) + } + + protected def notifyError(message: String, cause: OptionVal[Throwable], marker: OptionVal[LogMarker]): Unit = { + val error = cause match { + case OptionVal.Some(cause) ⇒ + marker match { + case OptionVal.Some(m) ⇒ Error(cause, logSource, logClass, message, mdc, m.asInstanceOf[UntypedLM]) + case OptionVal.None ⇒ Error(cause, logSource, logClass, message, mdc) + } + case OptionVal.None ⇒ + marker match { + case OptionVal.Some(m) ⇒ Error(logSource, logClass, message, mdc, m.asInstanceOf[UntypedLM]) + case OptionVal.None ⇒ Error(logSource, logClass, message, mdc) + } + } + bus.publish(error) + } + + @Deprecated + @deprecated("Use the 3 argument version instead", since = "2.5.10") + protected def notifyWarning(message: String, marker: OptionVal[LogMarker]): Unit = + notifyWarning(message, marker, OptionVal.None) + + protected def notifyWarning(message: String, marker: OptionVal[LogMarker], cause: OptionVal[Throwable]): Unit = { + val warning = + if (cause.isDefined) Warning(cause.get, logSource, logClass, message, mdc, marker.orNull.asInstanceOf[UntypedLM]) + else marker match { + case OptionVal.Some(m) ⇒ Warning(logSource, logClass, message, mdc, m.asInstanceOf[UntypedLM]) + case OptionVal.None ⇒ Warning(logSource, logClass, message, mdc) + } + bus.publish(warning) + } + + protected def notifyInfo(message: String, marker: OptionVal[LogMarker]): Unit = { + val info = marker match { + case OptionVal.Some(m) ⇒ Info(logSource, logClass, message, mdc, m.asInstanceOf[UntypedLM]) + case OptionVal.None ⇒ Info(logSource, logClass, message, mdc) + } + bus.publish(info) + } + + protected def notifyDebug(message: String, marker: OptionVal[LogMarker]): Unit = { + val debug = marker match { + case OptionVal.Some(m) ⇒ Debug(logSource, logClass, message, mdc, m.asInstanceOf[UntypedLM]) + case OptionVal.None ⇒ Debug(logSource, logClass, message, mdc) + } + bus.publish(debug) + } + + /** + * If `arg` is an `Array` it will be expanded into replacement arguments, which is useful when + * there are more than four arguments. + */ + private def format(t: String, arg1: Any): String = arg1 match { + case a: Array[_] if !a.getClass.getComponentType.isPrimitive ⇒ formatArray(t, a: _*) + case a: Array[_] ⇒ formatArray(t, (a map (_.asInstanceOf[AnyRef]): _*)) + case x ⇒ formatArray(t, x) + } + private def format(t: String, arg1: Any, arg2: Any): String = formatArray(t, arg1, arg2) + private def format(t: String, arg1: Any, arg2: Any, arg3: Any): String = formatArray(t, arg1, arg2, arg3) + private def format(t: String, arg1: Any, arg2: Any, arg3: Any, arg4: Any): String = formatArray(t, arg1, arg2, arg3, arg4) + + private def formatArray(t: String, arg: Any*): String = { + val sb = new java.lang.StringBuilder(64) + var p = 0 + var startIndex = 0 + while (p < arg.length) { + val index = t.indexOf("{}", startIndex) + if (index == -1) { + sb.append(t.substring(startIndex, t.length)) + .append(" WARNING arguments left: ") + .append(arg.length - p) + p = arg.length + startIndex = t.length + } else { + sb.append(t.substring(startIndex, index)) + .append(arg(p)) + startIndex = index + 2 + p += 1 + } + } + sb.append(t.substring(startIndex, t.length)).toString + } + +} diff --git a/akka-actor-typed/src/main/scala/akka/actor/typed/internal/receptionist/ReceptionistImpl.scala b/akka-actor-typed/src/main/scala/akka/actor/typed/internal/receptionist/ReceptionistImpl.scala index 4bbf8b2e7f..783db620ae 100644 --- a/akka-actor-typed/src/main/scala/akka/actor/typed/internal/receptionist/ReceptionistImpl.scala +++ b/akka-actor-typed/src/main/scala/akka/actor/typed/internal/receptionist/ReceptionistImpl.scala @@ -118,7 +118,7 @@ private[akka] object ReceptionistImpl extends ReceptionistBehaviorProvider { immutable[AllCommands] { (ctx, msg) ⇒ msg match { case Register(key, serviceInstance, replyTo) ⇒ - ctx.system.log.debug("[{}] Actor was registered: {} {}", ctx.self, key, serviceInstance) + ctx.log.debug("Actor was registered: {} {}", key, serviceInstance) watchWith(ctx, serviceInstance, RegisteredActorTerminated(key, serviceInstance)) replyTo ! Registered(key, serviceInstance) externalInterface.onRegister(key, serviceInstance) @@ -132,7 +132,7 @@ private[akka] object ReceptionistImpl extends ReceptionistBehaviorProvider { case externalInterface.RegistrationsChangedExternally(changes, state) ⇒ - ctx.system.log.debug("[{}] Registration changed: {}", ctx.self, changes) + ctx.log.debug("Registration changed: {}", changes) // FIXME: get rid of casts def makeChanges(registry: LocalServiceRegistry): LocalServiceRegistry = @@ -144,7 +144,7 @@ private[akka] object ReceptionistImpl extends ReceptionistBehaviorProvider { updateRegistry(changes.keySet, makeChanges) // overwrite all changed keys case RegisteredActorTerminated(key, serviceInstance) ⇒ - ctx.system.log.debug("[{}] Registered actor terminated: {} {}", ctx.self, key, serviceInstance) + ctx.log.debug("Registered actor terminated: {} {}", key, serviceInstance) externalInterface.onUnregister(key, serviceInstance) updateRegistry(Set(key), _.removed(key)(serviceInstance)) diff --git a/akka-actor-typed/src/main/scala/akka/actor/typed/javadsl/ActorContext.scala b/akka-actor-typed/src/main/scala/akka/actor/typed/javadsl/ActorContext.scala index 605f27682f..15ad3d9b45 100644 --- a/akka-actor-typed/src/main/scala/akka/actor/typed/javadsl/ActorContext.scala +++ b/akka-actor-typed/src/main/scala/akka/actor/typed/javadsl/ActorContext.scala @@ -10,6 +10,7 @@ import akka.annotation.ApiMayChange import akka.actor.typed._ import java.util.Optional +import akka.event.LoggingAdapter import akka.util.Timeout import scala.concurrent.duration.FiniteDuration @@ -60,6 +61,11 @@ trait ActorContext[T] { */ def getSystem: ActorSystem[Void] + /** + * An actor specific logger + */ + def getLog: Logger + /** * The list of child Actors created by this Actor during its lifetime that * are still alive, in no particular order. diff --git a/akka-actor-typed/src/main/scala/akka/actor/typed/javadsl/Behaviors.scala b/akka-actor-typed/src/main/scala/akka/actor/typed/javadsl/Behaviors.scala index 15fba3a969..017d9369ae 100644 --- a/akka-actor-typed/src/main/scala/akka/actor/typed/javadsl/Behaviors.scala +++ b/akka-actor-typed/src/main/scala/akka/actor/typed/javadsl/Behaviors.scala @@ -16,9 +16,9 @@ import akka.actor.typed.Signal import akka.actor.typed.ActorRef import akka.actor.typed.SupervisorStrategy import akka.actor.typed.scaladsl.{ ActorContext ⇒ SAC } -import akka.actor.typed.internal.{ BehaviorImpl, Supervisor, TimerSchedulerImpl } +import akka.actor.typed.internal.{ BehaviorImpl, LoggingBehaviorImpl, Supervisor, TimerSchedulerImpl } import akka.annotation.ApiMayChange - +import scala.collection.JavaConverters._ /** * Factories for [[akka.actor.typed.Behavior]]. */ @@ -316,4 +316,19 @@ object Behaviors { def receiveMessage(msg: T): Behavior[T] def receiveSignal(msg: Signal): Behavior[T] } + + /** + * Provide a MDC ("Mapped Diagnostic Context") for logging from the actor. + * + * @param mdcForMessage Is invoked before each message to setup MDC which is then attachd to each logging statement + * done for that message through the [[ActorContext.getLog]]. After the message has been processed + * the MDC is cleared. + * @param behavior The behavior that this should be applied to. + */ + def withMdc[T]( + `type`: Class[T], + mdcForMessage: akka.japi.function.Function[T, java.util.Map[String, Object]], + behavior: Behavior[T]): Behavior[T] = + LoggingBehaviorImpl.withMdc[T](message ⇒ mdcForMessage.apply(message).asScala.toMap, behavior)(ClassTag(`type`)) + } diff --git a/akka-actor-typed/src/main/scala/akka/actor/typed/scaladsl/ActorContext.scala b/akka-actor-typed/src/main/scala/akka/actor/typed/scaladsl/ActorContext.scala index 6c3e3dad59..78d1f3643a 100644 --- a/akka-actor-typed/src/main/scala/akka/actor/typed/scaladsl/ActorContext.scala +++ b/akka-actor-typed/src/main/scala/akka/actor/typed/scaladsl/ActorContext.scala @@ -3,16 +3,16 @@ */ package akka.actor.typed.scaladsl +import akka.actor.typed._ +import akka.annotation.{ ApiMayChange, DoNotInherit } +import akka.event.LoggingAdapter +import akka.util.Timeout + import scala.concurrent.ExecutionContextExecutor import scala.concurrent.duration.FiniteDuration import scala.reflect.ClassTag import scala.util.Try - -import akka.actor.typed._ -import akka.annotation.ApiMayChange -import akka.annotation.DoNotInherit import akka.annotation.InternalApi -import akka.util.Timeout /** * An Actor is given by the combination of a [[Behavior]] and a context in @@ -58,6 +58,11 @@ trait ActorContext[T] { this: akka.actor.typed.javadsl.ActorContext[T] ⇒ */ def system: ActorSystem[Nothing] + /** + * An actor specific logger + */ + def log: Logger + /** * The list of child Actors created by this Actor during its lifetime that * are still alive, in no particular order. diff --git a/akka-actor-typed/src/main/scala/akka/actor/typed/scaladsl/Behaviors.scala b/akka-actor-typed/src/main/scala/akka/actor/typed/scaladsl/Behaviors.scala index bcec5a4e6d..347f5283ca 100644 --- a/akka-actor-typed/src/main/scala/akka/actor/typed/scaladsl/Behaviors.scala +++ b/akka-actor-typed/src/main/scala/akka/actor/typed/scaladsl/Behaviors.scala @@ -5,7 +5,7 @@ package akka.actor.typed package scaladsl import akka.annotation.{ ApiMayChange, InternalApi } -import akka.actor.typed.internal.{ BehaviorImpl, Supervisor, TimerSchedulerImpl } +import akka.actor.typed.internal.{ BehaviorImpl, LoggingBehaviorImpl, Supervisor, TimerSchedulerImpl } import scala.reflect.ClassTag import scala.util.control.Exception.Catcher @@ -267,6 +267,19 @@ object Behaviors { def withTimers[T](factory: TimerScheduler[T] ⇒ Behavior[T]): Behavior[T] = TimerSchedulerImpl.withTimers(factory) + /** + * Provide a MDC ("Mapped Diagnostic Context") for logging from the actor. + * + * @param mdcForMessage Is invoked before each message to setup MDC which is then attachd to each logging statement + * done for that message through the [[ActorContext.log]]. After the message has been processed + * the MDC is cleared. + * @param behavior The behavior that this should be applied to. + */ + def withMdc[T: ClassTag]( + mdcForMessage: T ⇒ Map[String, Any], + behavior: Behavior[T]): Behavior[T] = + LoggingBehaviorImpl.withMdc(mdcForMessage, behavior) + // TODO // final case class Selective[T](timeout: FiniteDuration, selector: PartialFunction[T, Behavior[T]], onTimeout: () ⇒ Behavior[T]) diff --git a/akka-actor-typed/src/main/scala/akka/event/typed/EventStream.scala b/akka-actor-typed/src/main/scala/akka/event/typed/EventStream.scala deleted file mode 100644 index 40a71b72d3..0000000000 --- a/akka-actor-typed/src/main/scala/akka/event/typed/EventStream.scala +++ /dev/null @@ -1,87 +0,0 @@ -package akka.event.typed - -import akka.actor.typed.ActorRef -import akka.event.Logging.LogLevel - -/** - * An EventStream allows local actors to register for certain message types, including - * their subtypes automatically. Publishing events will broadcast them to all - * currently subscribed actors with matching subscriptions for the event type. - * - * IMPORTANT NOTICE - * - * This EventStream is local to the ActorSystem, it does not span a cluster. For - * disseminating messages across a cluster please refer to the DistributedPubSub - * module. - */ -trait EventStream { - /** - * Attempts to register the subscriber to the specified Classifier - * @return true if successful and false if not (because it was already - * subscribed to that Classifier, or otherwise) - */ - def subscribe[T](subscriber: ActorRef[T], to: Class[T]): Boolean - - /** - * Attempts to deregister the subscriber from the specified Classifier - * @return true if successful and false if not (because it wasn't subscribed - * to that Classifier, or otherwise) - */ - def unsubscribe[T](subscriber: ActorRef[T], from: Class[T]): Boolean - - /** - * Attempts to deregister the subscriber from all Classifiers it may be subscribed to - */ - def unsubscribe[T](subscriber: ActorRef[T]): Unit - - /** - * Publishes the specified Event to this bus - */ - def publish[T](event: T): Unit - - /** - * Query the current minimum log level. - */ - def logLevel: LogLevel - - /** - * Change the current minimum log level. - */ - def setLogLevel(loglevel: LogLevel): Unit -} - -import akka.actor.typed.{ ActorRef, Behavior, Settings } -import akka.event.Logging.LogEvent -import akka.{ event ⇒ e } - -abstract class Logger { - def initialBehavior: Behavior[Logger.Command] -} - -object Logger { - sealed trait Command - case class Initialize(eventStream: EventStream, replyTo: ActorRef[ActorRef[LogEvent]]) extends Command - // FIXME add Mute/Unmute (i.e. the TestEventListener functionality) -} - -class DefaultLoggingFilter(settings: Settings, eventStream: EventStream) extends e.DefaultLoggingFilter(() ⇒ eventStream.logLevel) - -/** - * [[akka.event.LoggingAdapter]] that publishes [[akka.event.Logging.LogEvent]] to event stream. - */ -class BusLogging(val bus: EventStream, val logSource: String, val logClass: Class[_], loggingFilter: e.LoggingFilter) - extends e.LoggingAdapter { - - import e.Logging._ - - def isErrorEnabled = loggingFilter.isErrorEnabled(logClass, logSource) - def isWarningEnabled = loggingFilter.isWarningEnabled(logClass, logSource) - def isInfoEnabled = loggingFilter.isInfoEnabled(logClass, logSource) - def isDebugEnabled = loggingFilter.isDebugEnabled(logClass, logSource) - - protected def notifyError(message: String): Unit = bus.publish(Error(logSource, logClass, message, mdc)) - protected def notifyError(cause: Throwable, message: String): Unit = bus.publish(Error(cause, logSource, logClass, message, mdc)) - protected def notifyWarning(message: String): Unit = bus.publish(Warning(logSource, logClass, message, mdc)) - protected def notifyInfo(message: String): Unit = bus.publish(Info(logSource, logClass, message, mdc)) - protected def notifyDebug(message: String): Unit = bus.publish(Debug(logSource, logClass, message, mdc)) -} diff --git a/akka-actor/src/main/scala/akka/event/Logging.scala b/akka-actor/src/main/scala/akka/event/Logging.scala index 1e32da50da..46545f9de3 100644 --- a/akka-actor/src/main/scala/akka/event/Logging.scala +++ b/akka-actor/src/main/scala/akka/event/Logging.scala @@ -739,17 +739,21 @@ object Logging { } + trait LogEventWithCause { + def cause: Throwable + } + /** * For ERROR Logging */ - case class Error(cause: Throwable, logSource: String, logClass: Class[_], message: Any = "") extends LogEvent { + case class Error(override val cause: Throwable, logSource: String, logClass: Class[_], message: Any = "") extends LogEvent with LogEventWithCause { def this(logSource: String, logClass: Class[_], message: Any) = this(Error.NoCause, logSource, logClass, message) override def level = ErrorLevel } - class Error2(cause: Throwable, logSource: String, logClass: Class[_], message: Any = "", override val mdc: MDC) extends Error(cause, logSource, logClass, message) { + class Error2(override val cause: Throwable, logSource: String, logClass: Class[_], message: Any = "", override val mdc: MDC) extends Error(cause, logSource, logClass, message) { def this(logSource: String, logClass: Class[_], message: Any, mdc: MDC) = this(Error.NoCause, logSource, logClass, message, mdc) } - class Error3(cause: Throwable, logSource: String, logClass: Class[_], message: Any, override val mdc: MDC, override val marker: LogMarker) + class Error3(override val cause: Throwable, logSource: String, logClass: Class[_], message: Any, override val mdc: MDC, override val marker: LogMarker) extends Error2(cause, logSource, logClass, message, mdc) with LogEventWithMarker { def this(logSource: String, logClass: Class[_], message: Any, mdc: MDC, marker: LogMarker) = this(Error.NoCause, logSource, logClass, message, mdc, marker) } @@ -784,9 +788,14 @@ object Logging { class Warning2(logSource: String, logClass: Class[_], message: Any, override val mdc: MDC) extends Warning(logSource, logClass, message) class Warning3(logSource: String, logClass: Class[_], message: Any, override val mdc: MDC, override val marker: LogMarker) extends Warning2(logSource, logClass, message, mdc) with LogEventWithMarker + class Warning4(logSource: String, logClass: Class[_], message: Any, override val mdc: MDC, override val marker: LogMarker, override val cause: Throwable) + extends Warning2(logSource, logClass, message, mdc) with LogEventWithMarker with LogEventWithCause object Warning { def apply(logSource: String, logClass: Class[_], message: Any, mdc: MDC) = new Warning2(logSource, logClass, message, mdc) def apply(logSource: String, logClass: Class[_], message: Any, mdc: MDC, marker: LogMarker) = new Warning3(logSource, logClass, message, mdc, marker) + + def apply(cause: Throwable, logSource: String, logClass: Class[_], message: Any, mdc: MDC) = new Warning4(logSource, logClass, message, mdc, null, cause) + def apply(cause: Throwable, logSource: String, logClass: Class[_], message: Any, mdc: MDC, marker: LogMarker) = new Warning4(logSource, logClass, message, mdc, marker, cause) } /** diff --git a/akka-slf4j/src/main/scala/akka/event/slf4j/Slf4jLogger.scala b/akka-slf4j/src/main/scala/akka/event/slf4j/Slf4jLogger.scala index ffc6ffd09c..3b20e68dfe 100644 --- a/akka-slf4j/src/main/scala/akka/event/slf4j/Slf4jLogger.scala +++ b/akka-slf4j/src/main/scala/akka/event/slf4j/Slf4jLogger.scala @@ -72,7 +72,10 @@ class Slf4jLogger extends Actor with SLF4JLogging with RequiresMessageQueue[Logg case event @ Warning(logSource, logClass, message) ⇒ withMdc(logSource, event) { - Logger(logClass, logSource).warn(markerIfPresent(event), "{}", message.asInstanceOf[AnyRef]) + event match { + case e: LogEventWithCause ⇒ Logger(logClass, logSource).warn(markerIfPresent(event), if (message != null) message.toString else null, e.cause) + case _ ⇒ Logger(logClass, logSource).warn(markerIfPresent(event), if (message != null) message.toString else null) + } } case event @ Info(logSource, logClass, message) ⇒ diff --git a/akka-testkit-typed/src/main/scala/akka/actor/typed/internal/ActorSystemStub.scala b/akka-testkit-typed/src/main/scala/akka/actor/typed/internal/ActorSystemStub.scala index 7c795b9878..cf6715c81a 100644 --- a/akka-testkit-typed/src/main/scala/akka/actor/typed/internal/ActorSystemStub.scala +++ b/akka-testkit-typed/src/main/scala/akka/actor/typed/internal/ActorSystemStub.scala @@ -7,8 +7,8 @@ package internal import java.util.concurrent.{ CompletionStage, ThreadFactory } import akka.annotation.InternalApi -import akka.event.Logging -import akka.event.typed.{ BusLogging, DefaultLoggingFilter, EventStream } +import akka.event.{ BusLogging, DefaultLoggingFilter, Logging } +import akka.testkit.typed.StubbedLogger import akka.util.Timeout import akka.{ actor ⇒ a, event ⇒ e } import com.typesafe.config.ConfigFactory @@ -43,16 +43,7 @@ import scala.concurrent._ } override def dynamicAccess: a.DynamicAccess = new a.ReflectiveDynamicAccess(getClass.getClassLoader) - override def eventStream: EventStream = new EventStream { - override def subscribe[T](subscriber: ActorRef[T], to: Class[T]) = false - override def setLogLevel(loglevel: Logging.LogLevel): Unit = {} - override def logLevel = Logging.InfoLevel - override def unsubscribe[T](subscriber: ActorRef[T], from: Class[T]) = false - override def unsubscribe[T](subscriber: ActorRef[T]): Unit = {} - override def publish[T](event: T): Unit = {} - } - override def logFilter: e.LoggingFilter = new DefaultLoggingFilter(settings, eventStream) - override def log: e.LoggingAdapter = new BusLogging(eventStream, path.parent.toString, getClass, logFilter) + override def logConfiguration(): Unit = log.info(settings.toString) override def scheduler: a.Scheduler = throw new UnsupportedOperationException("no scheduler") @@ -84,4 +75,6 @@ import scala.concurrent._ def hasExtension(ext: ExtensionId[_ <: Extension]): Boolean = throw new UnsupportedOperationException("ActorSystemStub cannot register extensions") + + def log: Logger = new StubbedLogger } diff --git a/akka-testkit-typed/src/main/scala/akka/testkit/typed/BehaviourTestkit.scala b/akka-testkit-typed/src/main/scala/akka/testkit/typed/BehaviourTestkit.scala index 6432d69360..88979ea373 100644 --- a/akka-testkit-typed/src/main/scala/akka/testkit/typed/BehaviourTestkit.scala +++ b/akka-testkit-typed/src/main/scala/akka/testkit/typed/BehaviourTestkit.scala @@ -7,13 +7,13 @@ import java.util.concurrent.ConcurrentLinkedQueue import akka.actor.typed.{ ActorRef, Behavior, PostStop, Props, Signal } import akka.annotation.{ ApiMayChange, InternalApi } +import akka.event.LoggingAdapter import scala.annotation.tailrec import scala.collection.immutable import scala.util.control.Exception.Catcher import scala.util.control.NonFatal import scala.concurrent.duration.{ Duration, FiniteDuration } - import scala.language.existentials /** diff --git a/akka-testkit-typed/src/main/scala/akka/testkit/typed/StubbedActorContext.scala b/akka-testkit-typed/src/main/scala/akka/testkit/typed/StubbedActorContext.scala index 113bbdce8d..b6ad6e187c 100644 --- a/akka-testkit-typed/src/main/scala/akka/testkit/typed/StubbedActorContext.scala +++ b/akka-testkit-typed/src/main/scala/akka/testkit/typed/StubbedActorContext.scala @@ -3,14 +3,17 @@ package akka.testkit.typed import akka.actor.InvalidMessageException import akka.{ actor ⇒ untyped } import akka.actor.typed._ -import akka.util.Helpers +import akka.util.{ Helpers, OptionVal } import akka.{ actor ⇒ a } import scala.collection.immutable.TreeMap import scala.concurrent.ExecutionContextExecutor import scala.concurrent.duration.FiniteDuration import akka.annotation.InternalApi -import akka.actor.typed.internal.{ ActorContextImpl, ActorRefImpl, ActorSystemStub, SystemMessage } +import akka.actor.typed.internal._ +import akka.actor.typed.internal.adapter.LoggerAdapterImpl +import akka.event.Logging.{ Info, LogEvent, LogLevel } +import akka.event.{ Logging, LoggingAdapter } /** * A local synchronous ActorRef that invokes the given function for every message send. @@ -32,6 +35,39 @@ private[akka] final class FunctionRef[-T]( override def isLocal = true } +final case class CapturedLogEvent(logLevel: LogLevel, message: String, cause: OptionVal[Throwable], marker: OptionVal[LogMarker]) + +/** + * INTERNAL API + * + * Captures log events for test inspection + */ +@InternalApi private[akka] final class StubbedLogger extends LoggerAdapterImpl(null, null, null, null) { + + private var logBuffer: List[CapturedLogEvent] = Nil + + override def isErrorEnabled: Boolean = true + override def isWarningEnabled: Boolean = true + override def isInfoEnabled: Boolean = true + override def isDebugEnabled: Boolean = true + + override protected def notifyError(message: String, cause: OptionVal[Throwable], marker: OptionVal[LogMarker]): Unit = + logBuffer = CapturedLogEvent(Logging.ErrorLevel, message, cause, marker) :: logBuffer + + override protected def notifyWarning(message: String, marker: OptionVal[LogMarker], cause: OptionVal[Throwable]): Unit = + logBuffer = CapturedLogEvent(Logging.WarningLevel, message, OptionVal.None, marker) :: logBuffer + + override protected def notifyInfo(message: String, marker: OptionVal[LogMarker]): Unit = + logBuffer = CapturedLogEvent(Logging.InfoLevel, message, OptionVal.None, marker) :: logBuffer + + override protected def notifyDebug(message: String, marker: OptionVal[LogMarker]): Unit = + logBuffer = CapturedLogEvent(Logging.DebugLevel, message, OptionVal.None, marker) :: logBuffer + + def logEntries: List[CapturedLogEvent] = logBuffer.reverse + def clearLog(): Unit = logBuffer = Nil + +} + /** * INTERNAL API * @@ -56,6 +92,7 @@ private[akka] final class FunctionRef[-T]( private var _children = TreeMap.empty[String, TestInbox[_]] private val childName = Iterator from 0 map (Helpers.base64(_)) + private val loggingAdapter = new StubbedLogger override def children: Iterable[ActorRef[Nothing]] = _children.values map (_.ref) def childrenNames: Iterable[String] = _children.keys @@ -138,4 +175,17 @@ private[akka] final class FunctionRef[-T]( def removeChildInbox(child: ActorRef[Nothing]): Unit = _children -= child.path.name override def toString: String = s"Inbox($self)" + + override def log: Logger = loggingAdapter + + /** + * The log entries logged through ctx.log.{debug, info, warn, error} are captured and can be inspected through + * this method. + */ + def logEntries: List[CapturedLogEvent] = loggingAdapter.logEntries + + /** + * Clear the log entries + */ + def clearLog(): Unit = loggingAdapter.clearLog() } diff --git a/akka-testkit-typed/src/main/scala/akka/testkit/typed/TestEventListener.scala b/akka-testkit-typed/src/main/scala/akka/testkit/typed/TestEventListener.scala deleted file mode 100644 index fa5b4163fc..0000000000 --- a/akka-testkit-typed/src/main/scala/akka/testkit/typed/TestEventListener.scala +++ /dev/null @@ -1,68 +0,0 @@ -package akka.testkit.typed - -import akka.event.Logging.{ LogEvent, StdOutLogger } -import akka.testkit.{ EventFilter, TestEvent ⇒ TE } -import akka.event.typed.Logger.{ Command, Initialize } - -import scala.annotation.tailrec -import akka.actor.typed.scaladsl.Behaviors -import akka.actor.typed.Behavior -import akka.event.typed.Logger - -/** - * EventListener for running tests, which allows selectively filtering out - * expected messages. To use it, include something like this into - * your config - * - *

- * akka.typed {
- *   loggers = ["akka.testkit.typed.TestEventListener"]
- * }
- * 
- */ -class TestEventListener extends Logger with StdOutLogger { - - override val initialBehavior: Behavior[Command] = { - Behaviors.deferred[Command] { _ ⇒ - Behaviors.immutable[Command] { - case (ctx, Initialize(eventStream, replyTo)) ⇒ - val log = ctx.spawn(Behaviors.deferred[AnyRef] { childCtx ⇒ - var filters: List[EventFilter] = Nil - - def filter(event: LogEvent): Boolean = filters exists (f ⇒ try { f(event) } catch { case e: Exception ⇒ false }) - - def addFilter(filter: EventFilter): Unit = filters ::= filter - - def removeFilter(filter: EventFilter) { - @tailrec def removeFirst(list: List[EventFilter], zipped: List[EventFilter] = Nil): List[EventFilter] = list match { - case head :: tail if head == filter ⇒ tail.reverse_:::(zipped) - case head :: tail ⇒ removeFirst(tail, head :: zipped) - case Nil ⇒ filters // filter not found, just return original list - } - filters = removeFirst(filters) - } - - Behaviors.immutable[AnyRef] { - case (_, TE.Mute(filters)) ⇒ - filters foreach addFilter - Behaviors.same - case (_, TE.UnMute(filters)) ⇒ - filters foreach removeFilter - Behaviors.same - case (_, event: LogEvent) ⇒ - if (!filter(event)) print(event) - Behaviors.same - case _ ⇒ Behaviors.unhandled - } - }, "logger") - - eventStream.subscribe(log, classOf[TE.Mute]) - eventStream.subscribe(log, classOf[TE.UnMute]) - ctx.watch(log) // sign death pact - replyTo ! log - - Behaviors.empty - } - } - } -}