Reference docs for SL4J logging in Typed, #27648 (#27696)

* Reference docs for SL4J logging in Typed, #27648
* mention package implicit for LoggerOps
* reference docs of log testing utilities
* cleanup classic logging.md
  * most of it is still relevant, and different enough from
    typed/logging.md to keep it separate
* use ThresholdFilter instead of LevelFilter

Co-Authored-By: Will Sargent <will.sargent@gmail.com>
This commit is contained in:
Patrik Nordwall 2019-09-17 17:26:13 +02:00 committed by GitHub
parent ea7704928b
commit 8cb2721c33
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 998 additions and 30 deletions

View file

@ -8,20 +8,24 @@ import static jdocs.akka.actor.testkit.typed.javadsl.AsyncTestingExampleTest.Pin
import static jdocs.akka.actor.testkit.typed.javadsl.AsyncTestingExampleTest.Pong;
import static jdocs.akka.actor.testkit.typed.javadsl.AsyncTestingExampleTest.echoActor;
// #junit-integration
import akka.actor.testkit.typed.javadsl.LogCapturing;
import org.junit.Rule;
// #junit-integration
import akka.actor.testkit.typed.javadsl.TestKitJunitResource;
import akka.actor.testkit.typed.javadsl.TestProbe;
import akka.actor.typed.ActorRef;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
public class JunitIntegrationExampleTest {
@ClassRule public static final TestKitJunitResource testKit = new TestKitJunitResource();
// #junit-integration
// this is shown in LogCapturingExampleTest
@Rule public final LogCapturing logCapturing = new LogCapturing();
// #junit-integration
@Test
public void testSomething() {

View file

@ -0,0 +1,32 @@
/*
* Copyright (C) 2009-2019 Lightbend Inc. <https://www.lightbend.com>
*/
package jdocs.akka.actor.testkit.typed.javadsl;
import static jdocs.akka.actor.testkit.typed.javadsl.AsyncTestingExampleTest.*;
// #log-capturing
import akka.actor.testkit.typed.javadsl.LogCapturing;
import akka.actor.testkit.typed.javadsl.TestKitJunitResource;
import akka.actor.testkit.typed.javadsl.TestProbe;
import akka.actor.typed.ActorRef;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
public class LogCapturingExampleTest {
@ClassRule public static final TestKitJunitResource testKit = new TestKitJunitResource();
@Rule public final LogCapturing logCapturing = new LogCapturing();
@Test
public void testSomething() {
ActorRef<Ping> pinger = testKit.spawn(echoActor(), "ping");
TestProbe<Pong> probe = testKit.createTestProbe();
pinger.tell(new Ping("hello", probe.ref()));
probe.expectMessage(new Pong("hello"));
}
}
// #log-capturing

View file

@ -4,9 +4,8 @@
<statusListener class="ch.qos.logback.core.status.NopStatusListener" />
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>DEBUG</level>
<onMatch>DENY</onMatch>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<encoder>
<pattern>%date{ISO8601} %-5level %logger %X{akkaSource} %X{sourceThread} - %msg%n</pattern>

View file

@ -7,11 +7,15 @@ package docs.akka.actor.testkit.typed.scaladsl
import com.github.ghik.silencer.silent
import docs.akka.actor.testkit.typed.scaladsl.AsyncTestingExampleSpec.{ echoActor, Ping, Pong }
//#log-capturing
import akka.actor.testkit.typed.scaladsl.LogCapturing
//#scalatest-integration
import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
import org.scalatest.WordSpecLike
//#scalatest-integration
//#log-capturing
@silent
//#scalatest-integration
class ScalaTestIntegrationExampleSpec extends ScalaTestWithActorTestKit with WordSpecLike {
@ -26,3 +30,17 @@ class ScalaTestIntegrationExampleSpec extends ScalaTestWithActorTestKit with Wor
}
}
//#scalatest-integration
//#log-capturing
class LogCapturingExampleSpec extends ScalaTestWithActorTestKit with WordSpecLike with LogCapturing {
"Something" must {
"behave correctly" in {
val pinger = testKit.spawn(echoActor, "ping")
val probe = testKit.createTestProbe[Pong]()
pinger ! Ping("hello", probe.ref)
probe.expectMessage(Pong("hello"))
}
}
}
//#log-capturing

View file

@ -0,0 +1,191 @@
/*
* Copyright (C) 2019 Lightbend Inc. <https://www.lightbend.com>
*/
package jdocs.akka.typed;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import akka.actor.typed.ActorRef;
import akka.actor.typed.ActorSystem;
import akka.actor.typed.Behavior;
import akka.actor.typed.javadsl.AbstractBehavior;
import akka.actor.typed.javadsl.ActorContext;
import akka.actor.typed.javadsl.Behaviors;
import akka.actor.typed.javadsl.Receive;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
// #logMessages
import akka.actor.typed.LogOptions;
import org.slf4j.event.Level;
// #logMessages
// #test-logging
import akka.actor.testkit.typed.javadsl.LoggingEventFilter;
// #test-logging
public interface LoggingDocExamples {
// #context-log
public class MyLoggingBehavior extends AbstractBehavior<String> {
public static Behavior<String> create() {
return Behaviors.setup(MyLoggingBehavior::new);
}
private final ActorContext<String> context;
private MyLoggingBehavior(ActorContext<String> context) {
this.context = context;
}
@Override
public Receive<String> createReceive() {
return newReceiveBuilder().onMessage(String.class, this::onReceive).build();
}
private Behavior<String> onReceive(String message) {
context.getLog().info("Received message: {}", message);
return this;
}
}
// #context-log
// #logger-name
public class BackendManager extends AbstractBehavior<String> {
public static Behavior<String> create() {
return Behaviors.setup(
context -> {
context.setLoggerName(BackendManager.class);
context.getLog().info("Starting up");
return new BackendManager(context);
});
}
private final ActorContext<String> context;
private BackendManager(ActorContext<String> context) {
this.context = context;
}
@Override
public Receive<String> createReceive() {
return newReceiveBuilder().onMessage(String.class, this::onReceive).build();
}
private Behavior<String> onReceive(String message) {
context.getLog().debug("Received message: {}", message);
return this;
}
}
// #logger-name
// #logger-factory
class BackendTask {
private final Logger log = LoggerFactory.getLogger(getClass());
void run() {
CompletableFuture<String> task =
CompletableFuture.supplyAsync(
() -> {
// some work
return "result";
});
task.whenComplete(
(result, exc) -> {
if (exc == null) log.error("Task failed", exc);
else log.info("Task completed: {}", result);
});
}
}
// #logger-factory
static void logMessages() {
// #logMessages
Behaviors.logMessages(LogOptions.create().withLevel(Level.TRACE), BackendManager.create());
// #logMessages
}
public class BackendManager2 extends AbstractBehavior<BackendManager2.Command> {
interface Command {
String identifier();
}
public static Behavior<Command> create() {
return Behaviors.empty();
}
@Override
public Receive<Command> createReceive() {
return newReceiveBuilder().build();
}
}
static void withMdc() {
ActorSystem<Void> system = ActorSystem.create(Behaviors.empty(), "notUsed");
// #withMdc
Map<String, String> staticMdc = new HashMap<>();
staticMdc.put("startTime", String.valueOf(system.startTime()));
Behaviors.withMdc(
BackendManager2.Command.class,
staticMdc,
message -> {
Map<String, String> msgMdc = new HashMap<>();
msgMdc.put("identifier", message.identifier());
msgMdc.put("upTime", String.valueOf(system.uptime()));
return msgMdc;
},
BackendManager2.create());
// #withMdc
}
static class Message {
final String s;
public Message(String s) {
this.s = s;
}
}
static void logging() {
ActorSystem<Void> system = ActorSystem.create(Behaviors.empty(), "notUsed");
ActorRef<Message> ref = null;
// #test-logging
LoggingEventFilter.info("Received message")
.intercept(
system,
() -> {
ref.tell(new Message("hello"));
return null;
});
// #test-logging
// #test-logging-criteria
LoggingEventFilter.error(IllegalArgumentException.class)
.withMessageRegex(".*was rejected.*expecting ascii input.*")
.withCustom(
event ->
event.getMarker().isPresent()
&& event.getMarker().get().getName().equals("validation"))
.withOccurrences(2)
.intercept(
system,
() -> {
ref.tell(new Message("hellö"));
ref.tell(new Message("hejdå"));
return null;
});
// #test-logging-criteria
}
}

View file

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<encoder>
<pattern>[%date{ISO8601}] [%-5level] [%logger] [%thread] [%X{akkaSource}] - %msg%n</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>target/myapp-dev.log</file>
<encoder>
<pattern>[%date{ISO8601}] [%-5level] [%logger] [%thread] [%X{akkaSource}] - %msg%n</pattern>
</encoder>
</appender>
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<queueSize>1024</queueSize>
<neverBlock>true</neverBlock>
<appender-ref ref="FILE" />
</appender>
<root level="DEBUG">
<appender-ref ref="STDOUT"/>
<appender-ref ref="ASYNC"/>
</root>
</configuration>

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>myapp.log</file>
<immediateFlush>false</immediateFlush>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>myapp_%d{yyyy-MM-dd}.log</fileNamePattern>
</rollingPolicy>
<encoder>
<pattern>[%date{ISO8601}] [%-5level] [%logger] [%thread] [%X{akkaSource}] - %msg%n</pattern>
</encoder>
</appender>
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<queueSize>1024</queueSize>
<neverBlock>true</neverBlock>
<appender-ref ref="FILE" />
</appender>
<root level="INFO">
<appender-ref ref="ASYNC"/>
</root>
</configuration>

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<encoder>
<pattern>[%date{ISO8601}] [%-5level] [%logger] [%thread] [%X{akkaSource}] - %msg%n</pattern>
</encoder>
</appender>
<!--
Logging from tests are silenced by this appender. When there is a test failure
the captured logging events are flushed to the appenders defined for the
akka.actor.testkit.typed.internal.CapturingAppenderDelegate logger.
-->
<appender name="CapturingAppender" class="akka.actor.testkit.typed.internal.CapturingAppender" />
<!--
The appenders defined for this CapturingAppenderDelegate logger are used
when there is a test failure and all logging events from the test are
flushed to these appenders.
-->
<logger name="akka.actor.testkit.typed.internal.CapturingAppenderDelegate" >
<appender-ref ref="STDOUT"/>
</logger>
<root level="DEBUG">
<appender-ref ref="CapturingAppender"/>
</root>
</configuration>

View file

@ -4,9 +4,8 @@
<statusListener class="ch.qos.logback.core.status.NopStatusListener" />
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>DEBUG</level>
<onMatch>DENY</onMatch>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<encoder>
<pattern>%date{ISO8601} %-5level %logger %X{akkaSource} %X{sourceThread} - %msg%n</pattern>

View file

@ -0,0 +1,144 @@
/*
* Copyright (C) 2019 Lightbend Inc. <https://www.lightbend.com>
*/
package docs.akka.typed
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scala.util.Failure
import scala.util.Success
import akka.actor.typed.ActorRef
import akka.actor.typed.ActorSystem
import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.Behaviors
import org.slf4j.LoggerFactory
object LoggingDocExamples {
object BackendManager {
sealed trait Command {
def identifier: String
}
def apply(): Behavior[Command] = Behaviors.empty
}
def howToUse(): Unit = {
//#context-log
Behaviors.receive[String] { (context, message) =>
context.log.info("Received message: {}", message)
Behaviors.same
}
//#context-log
//#logger-name
Behaviors.setup[String] { context =>
context.setLoggerName("com.myservice.BackendManager")
context.log.info("Starting up")
Behaviors.receiveMessage { message =>
context.log.debug("Received message: {}", message)
Behaviors.same
}
}
//#logger-name
//#logger-factory
val log = LoggerFactory.getLogger("com.myservice.BackendTask")
Future {
// some work
"result"
}.onComplete {
case Success(result) => log.info("Task completed: {}", result)
case Failure(exc) => log.error("Task failed", exc)
}
//#logger-factory
}
def placeholders(): Unit = {
//#info2
import akka.actor.typed.scaladsl.LoggerOps
Behaviors.receive[String] { (context, message) =>
context.log.info2("{} received message: {}", context.self.path.name, message)
Behaviors.same
}
//#info2
//#infoN
import akka.actor.typed.scaladsl.LoggerOps
Behaviors.receive[String] { (context, message) =>
context.log.infoN(
"{} received message of size {} starting with: {}",
context.self.path.name,
message.length,
message.take(10))
Behaviors.same
}
//#infoN
}
def logMessages(): Unit = {
//#logMessages
import akka.actor.typed.LogOptions
import org.slf4j.event.Level
Behaviors.logMessages(LogOptions().withLevel(Level.TRACE), BackendManager())
//#logMessages
}
def withMdc(): Unit = {
val system: ActorSystem[_] = ???
//#withMdc
val staticMdc = Map("startTime" -> system.startTime.toString)
Behaviors.withMdc[BackendManager.Command](
staticMdc,
mdcForMessage =
(msg: BackendManager.Command) => Map("identifier" -> msg.identifier, "upTime" -> system.uptime.toString)) {
BackendManager()
}
//#withMdc
}
def logging(): Unit = {
implicit val system: ActorSystem[_] = ???
final case class Message(s: String)
val ref: ActorRef[Message] = ???
//#test-logging
import akka.actor.testkit.typed.scaladsl.LoggingEventFilter
// implicit ActorSystem is needed, but that is given by ScalaTestWithActorTestKit
//implicit val system: ActorSystem[_]
LoggingEventFilter.info("Received message").intercept {
ref ! Message("hello")
}
//#test-logging
//#test-logging-criteria
LoggingEventFilter
.error[IllegalArgumentException]
.withMessageRegex(".*was rejected.*expecting ascii input.*")
.withCustom { event =>
event.marker match {
case Some(m) => m.getName == "validation"
case None => false
}
}
.withOccurrences(2)
.intercept {
ref ! Message("hellö")
ref ! Message("hejdå")
}
//#test-logging-criteria
}
}

View file

@ -0,0 +1,18 @@
/*
* Copyright (C) 2019 Lightbend Inc. <https://www.lightbend.com>
*/
package docs.akka.typed
//#loggerops-package-implicit
import scala.language.implicitConversions
import akka.actor.typed.scaladsl.LoggerOps
import org.slf4j.Logger
package object myapp {
implicit def loggerOps(logger: Logger): LoggerOps =
LoggerOps(logger)
}
//#loggerops-package-implicit

View file

@ -4,9 +4,8 @@
<statusListener class="ch.qos.logback.core.status.NopStatusListener" />
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>DEBUG</level>
<onMatch>DENY</onMatch>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<encoder>
<pattern>%date{ISO8601} %-5level %logger %X{akkaSource} %X{sourceThread} - %msg%n</pattern>

View file

@ -4,9 +4,8 @@
<statusListener class="ch.qos.logback.core.status.NopStatusListener" />
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>DEBUG</level>
<onMatch>DENY</onMatch>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<encoder>
<pattern>%date{ISO8601} %-5level %logger %X{akkaSource} %X{sourceThread} { "persistenceId": "%X{persistenceId}", "persistencePhase": "%X{persistencePhase}" } - %msg%n</pattern>

View file

@ -4,9 +4,8 @@
<statusListener class="ch.qos.logback.core.status.NopStatusListener" />
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>DEBUG</level>
<onMatch>DENY</onMatch>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<encoder>
<pattern>%date{ISO8601} %-5level %logger %X{akkaSource} %X{sourceThread} - %msg%n</pattern>

View file

@ -4,7 +4,7 @@
@@@ index
* [logging](logging.md)
* [logging](typed/logging.md)
* [common/circuitbreaker](common/circuitbreaker.md)
@@@

View file

@ -1,4 +1,7 @@
# Logging
# Classic Logging
@@include[includes.md](includes.md) { #actor-api }
For the new API see @ref[Logging](typed/logging.md).
## Dependency
@ -336,7 +339,7 @@ It has a single dependency: the slf4j-api jar. In your runtime, you also need a
version="$akka.version$"
group2="ch.qos.logback"
artifact2="logback-classic"
version2="1.2.3"
version2="$logback_version$"
}
You need to enable the Slf4jLogger in the `loggers` element in
@ -352,6 +355,9 @@ If you set the `loglevel` to a higher level than "DEBUG", any DEBUG events will
out already at the source and will never reach the logging backend, regardless of how the backend
is configured.
You can enable `DEBUG` level for `akka.loglevel` and control the actual level in the SLF4J backend
without any significant overhead, also for production.
@@@
```ruby
@ -397,9 +403,31 @@ while the underlying infrastructure writes the log statements.
This can be avoided by configuring the logging implementation to use
a non-blocking appender. Logback provides [AsyncAppender](http://logback.qos.ch/manual/appenders.html#AsyncAppender)
that does this. It also contains a feature which will drop `INFO` and `DEBUG` messages if the logging
that does this.
### Logback configuration
Logback has flexible configuration options and details can be found in the
[Logback manual](https://logback.qos.ch/manual/configuration.html) and other external resources.
One part that is important to highlight is the importance of configuring an [AsyncAppender](http://logback.qos.ch/manual/appenders.html#AsyncAppender),
because it offloads rendering of logging events to a background thread, increasing performance. It doesn't block
the threads of the `ActorSystem` while the underlying infrastructure writes the log messages to disk or other configured
destination. It also contains a feature which will drop `INFO` and `DEBUG` messages if the logging
load is high.
A starting point for configuration of `logback.xml` for production:
@@snip [logback.xml](/akka-actor-typed-tests/src/test/resources/logback-doc-prod.xml)
For development you might want to log to standard out, but also have all debug level logging to file, like
in this example:
@@snip [logback.xml](/akka-actor-typed-tests/src/test/resources/logback-doc-dev.xml)
Place the `logback.xml` file in `src/main/resources/logback.xml`. For tests you can define different
logging configuration in `src/test/resources/logback-test.xml`.
### Logging Thread, Akka Source and Actor System in MDC
Since the logging is done asynchronously the thread in which the logging was performed is captured in
@ -458,7 +486,7 @@ If you want to more accurately output the timestamp, use the MDC attribute `akka
```
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%X{akkaTimestamp} %-5level %logger{36} %X{akkaSource} - %msg%n</pattern>
<pattern>%X{akkaTimestamp} %-5level %logger{36} %X{akkaTimestamp} - %msg%n</pattern>
</encoder>
</appender>
```

View file

@ -0,0 +1,393 @@
# Logging
## Dependency
To use Logging, you must at least use the Akka actors dependency in your project, and configure logging
via the SLF4J backend, such as Logback configuration.
@@dependency[sbt,Maven,Gradle] {
group="com.typesafe.akka"
artifact="akka-actor-typed_$scala.binary_version$"
version="$akka.version$"
}
## Introduction
[SLF4J](http://www.slf4j.org/) is used for logging and Akka provides access to an `org.slf4j.Logger` for a specific
actor via the `ActorContext`. You may also retrieve a `Logger` with the ordinary `org.slf4j.LoggerFactory`.
To ensure that logging has minimal performance impact it's important that you configure an
asynchronous appender for the SLF4J backend. Logging generally means IO and locks,
which can slow down the operations of your code if it was performed synchronously.
## How to log
The `ActorContext` provides access to an `org.slf4j.Logger` for a specific actor.
Scala
: @@snip [LoggingDocExamples.scala](/akka-actor-typed-tests/src/test/scala/docs/akka/typed/LoggingDocExamples.scala) { #context-log }
Java
: @@snip [LoggingDocExamples.java](/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/LoggingDocExamples.java) { #context-log }
The `Logger` via the `ActorContext` will automatically have a name that corresponds to the `Behavior` of the
actor when the log is accessed the first time. The class name when using `AbstractBehavior` or the class @scala[or object]
name where the `Behavior` is defined when using the functional style. You can set a custom logger name
with the `setLoggerName` of the `ActorContext`.
Scala
: @@snip [LoggingDocExamples.scala](/akka-actor-typed-tests/src/test/scala/docs/akka/typed/LoggingDocExamples.scala) { #logger-name }
Java
: @@snip [LoggingDocExamples.java](/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/LoggingDocExamples.java) { #logger-name }
The convention is to use logger names like fully qualified class names. The parameter to `setLoggerName`
can be a `String` or a `Class`, where the latter is convenience for the class name.
When logging via the `ActorContext` the path of the actor will automatically be added as `akkaSource`
Mapped Diagnostic Context (MDC) value. MDC is typically implemented with a `ThreadLocal` by the SLF4J backend.
To reduce performance impact this MDC value is set when you access the @scala[`log`]@java[`getLog`] method so
you shouldn't cache the returned `Logger` in your own field. That is handled by `ActorContext` and retrieving
the `Logger` repeatedly with the @scala[`log`]@java[`getLog`] method has low overhead.
The MDC is cleared automatically after processing of current message has finished.
@@@ note
The `Logger` is thread-safe but the @scala[`log`]@java[`getLog`] method in `ActorContext` is not
thread-safe and should not be accessed from threads other than the ordinary actor message processing
thread, such as @scala[`Future`]@java[`CompletionStage`] callbacks.
@@@
It's also perfectly fine to use a `Logger` retrieved via `org.slf4j.LoggerFactory`, but then the logging
events will not include the `akkaSource` MDC value. This is the recommend way when logging outside
of an actor, including logging from @scala[`Future`]@java[`CompletionStage`] callbacks.
Scala
: @@snip [LoggingDocExamples.scala](/akka-actor-typed-tests/src/test/scala/docs/akka/typed/LoggingDocExamples.scala) { #logger-factory }
Java
: @@snip [LoggingDocExamples.java](/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/LoggingDocExamples.java) { #logger-factory }
### Placeholder arguments
The log message may contain argument placeholders `{}`, which will be substituted if the log level is enabled.
Compared to constructing a full string for the log message this has the advantage of avoiding superfluous
string concatenation and object allocations when the log level is disabled. Some logging backends
may also use these message templates before argument substitution to group and filter logging events.
It can be good to know that 3 or more arguments will result in the relatively small cost of allocating
an array (vararg parameter) also when the log level is disabled. The methods with 1 or 2 arguments
don't allocate the vararg array.
@@@ div { .group-scala }
When using the methods for 2 argument placeholders the compiler will often not be able to select the
right method and report compiler error "ambiguous reference to overloaded definition". To work around this
problem you can use the `trace2`, `debug2`, `info2`, `warn2` or `error2` extension methods that are added
by `import akka.actor.typed.scaladsl.LoggerOps` or `import akka.actor.typed.scaladsl._`.
Scala
: @@snip [LoggingDocExamples.scala](/akka-actor-typed-tests/src/test/scala/docs/akka/typed/LoggingDocExamples.scala) { #info2 }
When using the methods for 3 or more argument placeholders, the compiler will not be able to convert
the method parameters to the vararg array when they contain primitive values such as `Int`,
and report compiler error "overloaded method value info with alternatives".
To work around this problem you can use the `traceN`, `debugN`, `infoN`, `warnN` or `errorN` extension
methods that are added by the same `LoggerOps` import.
Scala
: @@snip [LoggingDocExamples.scala](/akka-actor-typed-tests/src/test/scala/docs/akka/typed/LoggingDocExamples.scala) { #infoN }
If you find it tedious to add the import of `LoggerOps` at many places you can make those additional methods
available with a single implicit conversion placed in a root package object of your code:
Scala
: @@snip [package.scala](/akka-actor-typed-tests/src/test/scala/docs/akka/typed/myapp/package.scala) { #loggerops-package-implicit }
@@@
### Behaviors.logMessages
If you want very detailed logging of messages and signals you can decorate a `Behavior`
with `Behaviors.logMessages`.
Scala
: @@snip [LoggingDocExamples.scala](/akka-actor-typed-tests/src/test/scala/docs/akka/typed/LoggingDocExamples.scala) { #logMessages }
Java
: @@snip [LoggingDocExamples.java](/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/LoggingDocExamples.java) { #logMessages }
## Behaviors.withMdc
To include [MDC](http://logback.qos.ch/manual/mdc.html) attributes in logging events from an actor
you can decorate a `Behavior` with `Behaviors.withMdc` or use the `org.slf4j.MDC` API directly.
The `Behaviors.withMdc` decorator has two parts. A static `Map` of MDC attributes that are not changed,
and a dynamic `Map` that can be constructed for each message.
Scala
: @@snip [LoggingDocExamples.scala](/akka-actor-typed-tests/src/test/scala/docs/akka/typed/LoggingDocExamples.scala) { #withMdc }
Java
: @@snip [LoggingDocExamples.java](/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/LoggingDocExamples.java) { #withMdc }
If you use the MDC API directly, be aware of that MDC is typically implemented with a `ThreadLocal` by the SLF4J backend.
Akka clears the MDC if logging is performed via the @scala[`log`]@java[`getLog`] of the `ActorContext` the MDC is cleared
automatically after processing of current message has finished, but only if you accessed @scala[`log`]@java[`getLog`].
The entire MDC is cleared, including attributes that you add yourself to the MDC.
MDC is not cleared automatically if you use a `Logger` via `LoggerFactory` or not touch @scala[`log`]@java[`getLog`]
in the `ActorContext`.
## SLF4J backend
To ensure that logging has minimal performance impact it's important that you configure an
asynchronous appender for the SLF4J backend. Logging generally means IO and locks,
which can slow down the operations of your code if it was performed synchronously.
@@@ warning
For production the SLF4J backend should be configured with an asynchronous appender as described here.
Otherwise there is a risk of reduced performance and thread starvation problems of the dispatchers
that are running actors and other tasks.
@@@
### Logback
`akka-actor-typed` includes a dependency to the `slf4j-api`. In your runtime, you also need a SLF4J backend.
We recommend [Logback](http://logback.qos.ch/):
@@dependency[sbt,Maven,Gradle] {
group="ch.qos.logback"
artifact="logback-classic"
version="$logback_version$"
}
Logback has flexible configuration options and details can be found in the
[Logback manual](https://logback.qos.ch/manual/configuration.html) and other external resources.
One part that is important to highlight is the importance of configuring an [AsyncAppender](http://logback.qos.ch/manual/appenders.html#AsyncAppender),
because it offloads rendering of logging events to a background thread, increasing performance. It doesn't block
the threads of the `ActorSystem` while the underlying infrastructure writes the log messages to disk or other configured
destination. It also contains a feature which will drop `INFO` and `DEBUG` messages if the logging
load is high.
A starting point for configuration of `logback.xml` for production:
@@snip [logback.xml](/akka-actor-typed-tests/src/test/resources/logback-doc-prod.xml)
For development you might want to log to standard out, but also have all debug level logging to file, like
in this example:
@@snip [logback.xml](/akka-actor-typed-tests/src/test/resources/logback-doc-dev.xml)
Place the `logback.xml` file in `src/main/resources/logback.xml`. For tests you can define different
logging configuration in `src/test/resources/logback-test.xml`.
#### MDC values
When logging via the @scala[`log`]@java[`getLog`] of the `ActorContext` as described in
@ref:[How to log](#how-to-log) Akka captures the actor's path and includes that in MDC with
attribute name `akkaSource`.
This can be included in the log output with `%X{akkaSource}` specifier within the pattern layout configuration:
```
<encoder>
<pattern>%date{ISO8601} %-5level %logger{36} %X{akkaSource} - %msg%n</pattern>
</encoder>
```
## Internal logging by Akka
### Event bus
For historical reasons logging by the Akka internals and by classic actors are performed asynchronously
through an event bus. Such log events are processed by an event handler actor, which then emits them to
SLF4J or directly to standard out.
When `akka-actor-typed` is on the classpath this event handler actor will emit the events to SLF4J.
The `akka.event.slf4j.Slf4jLogger` and `akka.event.slf4j.Slf4jLoggingFilter` are enabled automatically
without additional configuration. This can be disabled by `akka.use-slf4j=off` configuration property.
In other words, you don't have to do anything for the Akka internal logging to end up in your configured
SLF4J backend.
### Log level
Ultimately the log level defined in the SLF4J backend is used. For the Akka internal logging it will
also check the level defined by the SLF4J backend before constructing the final log message and
emitting it to the event bus.
However, there is an additional `akka.loglevel` configuration property that defines if logging events
with lower log level should be discarded immediately without consulting the SLF4J backend. By default
this is at `INFO` level, which means that `DEBUG` level logging from the Akka internals will not
reach the SLF4J backend even if `DEBUG` is enabled in the backend.
You can enable `DEBUG` level for `akka.loglevel` and control the actual level in the SLF4j backend
without any significant overhead, also for production.
```
akka.loglevel = "DEBUG"
```
To turn off all Akka internal logging (not recommended) you can configure the log levels to be
`OFF` like this.
```
akka {
stdout-loglevel = "OFF"
loglevel = "OFF"
}
```
The `stdout-loglevel` is only in effect during system startup and shutdown, and setting
it to `OFF` as well, ensures that nothing gets logged during system startup or shutdown.
### Logging to stdout during startup and shutdown
When the actor system is starting up and shutting down the configured `loggers` are not used.
Instead log messages are printed to stdout (System.out). The default log level for this
stdout logger is `WARNING` and it can be silenced completely by setting
`akka.stdout-loglevel=OFF`.
### Logging of Dead Letters
By default messages sent to dead letters are logged at info level. Existence of dead letters
does not necessarily indicate a problem, but they are logged by default for the sake of caution.
After a few messages this logging is turned off, to avoid flooding the logs.
You can disable this logging completely or adjust how many dead letters are
logged. During system shutdown it is likely that you see dead letters, since pending
messages in the actor mailboxes are sent to dead letters. You can also disable logging
of dead letters during shutdown.
```
akka {
log-dead-letters = 10
log-dead-letters-during-shutdown = on
}
```
To customize the logging further or take other actions for dead letters you can subscribe
to the @ref:[Event Stream](../event-bus.md#event-stream).
### Auxiliary logging options
Akka has a few configuration options for very low level debugging. These make more sense in development than in production.
You almost definitely need to have logging set to DEBUG to use any of the options below:
```
akka {
loglevel = "DEBUG"
}
```
This config option is very good if you want to know what config settings are loaded by Akka:
```
akka {
# Log the complete configuration at INFO level when the actor system is started.
# This is useful when you are uncertain of what configuration is used.
log-config-on-start = on
}
```
If you want unhandled messages logged at DEBUG:
```
akka {
actor {
debug {
# enable DEBUG logging of unhandled messages
unhandled = on
}
}
}
```
If you want to monitor subscriptions (subscribe/unsubscribe) on the ActorSystem.eventStream:
```
akka {
actor {
debug {
# enable DEBUG logging of subscription changes on the eventStream
event-stream = on
}
}
}
```
<a id="logging-remote"></a>
### Auxiliary remote logging options
If you want to see all messages that are sent through remoting at DEBUG log level, use the following config option.
Note that this logs the messages as they are sent by the transport layer, not by an actor.
```
akka.remote.artery {
# If this is "on", Akka will log all outbound messages at DEBUG level,
# if off then they are not logged
log-sent-messages = on
}
```
If you want to see all messages that are received through remoting at DEBUG log level, use the following config option.
Note that this logs the messages as they are received by the transport layer, not by an actor.
```
akka.remote.artery {
# If this is "on", Akka will log all inbound messages at DEBUG level,
# if off then they are not logged
log-received-messages = on
}
```
### MDC values from Akka internal logging
Since the logging is done asynchronously the thread in which the logging was performed is captured in
MDC with attribute name `sourceThread`.
With Logback the thread name is available with `%X{sourceThread}` specifier within the pattern layout configuration:
```
<encoder>
<pattern>%date{ISO8601} %-5level %logger{36} %X{sourceThread} - %msg%n</pattern>
</encoder>
```
The path of the actor in which the logging was performed is available in the MDC with attribute name `akkaSource`,
With Logback the actor path is available with `%X{akkaSource}` specifier within the pattern layout configuration:
```
<encoder>
<pattern>%date{ISO8601} %-5level %logger{36} %X{akkaSource} - %msg%n</pattern>
</encoder>
```
The actor system in which the logging was performed is available in the MDC with attribute name `sourceActorSystem`,
but that is typically also included in the `akkaSource` attribute.
With Logback the ActorSystem name is available with `%X{sourceActorSystem}` specifier within the pattern layout configuration:
```
<encoder>
<pattern>%date{ISO8601} %-5level %logger{36} %X{sourceActorSystem} - %msg%n</pattern>
</encoder>
```
Akka's internal logging is asynchronous which means that the timestamp of a log entry is taken from
when the underlying logger implementation is called, which can be surprising at first.
If you want to more accurately output the timestamp, use the MDC attribute `akkaTimestamp`.
With Logback the timestamp is available with `%X{akkaTimestamp}` specifier within the pattern layout configuration:
```
<encoder>
<pattern>%X{akkaTimestamp} %-5level %logger{36} %X{akkaTimestamp} - %msg%n</pattern>
</encoder>
```
## Logging in tests
Testing utilities are described in @ref:[Testing](testing.md#test-of-logging).

View file

@ -225,6 +225,65 @@ Java
: @@snip [ManualTimerExampleTest.scala](/akka-actor-testkit-typed/src/test/java/jdocs/akka/actor/testkit/typed/javadsl/ManualTimerExampleTest.java) { #manual-scheduling-simple }
### Test of logging
To verify that certain @ref:[logging](logging.md) events are emitted there is a utility called `LoggingEventFilter`.
You define a criteria of the expected logging events and it will assert that the given number of occurrences
of matching logging events are emitted within a block of code.
@@@ note
The `LoggingEventFilter` implementation @ref:[requires Logback dependency](logging.md#logback).
@@@
For example, a criteria that verifies that an `INFO` level event with a message containing "Received message" is logged:
Scala
: @@snip [LoggingDocExamples.scala](/akka-actor-typed-tests/src/test/scala/docs/akka/typed/LoggingDocExamples.scala) { #test-logging }
Java
: @@snip [LoggingDocExamples.java](/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/LoggingDocExamples.java) { #test-logging }
More advanced criteria can be built by chaining conditions that all must be satisfied for a matching event.
Scala
: @@snip [LoggingDocExamples.scala](/akka-actor-typed-tests/src/test/scala/docs/akka/typed/LoggingDocExamples.scala) { #test-logging-criteria }
Java
: @@snip [LoggingDocExamples.java](/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/LoggingDocExamples.java) { #test-logging-criteria }
See @apidoc[typed.*.LoggingEventFilter] for more details.
### Silence logging output from tests
When running tests, it's typically preferred to have the output to standard out, together with the output from the
testing framework (@scala[ScalaTest]@java[JUnit]). On one hand you want the output to be clean without logging noise,
but on the other hand you want as much information as possible if there is a test failure (for example in CI builds).
The Akka TestKit provides a `LogCapturing` utility to support this with ScalaTest or JUnit. It will buffer log events instead
of emitting them to the `ConsoleAppender` immediately (or whatever Logback appender that is configured). When
there is a test failure the buffered events are flushed to the target appenders, typically a `ConsoleAppender`.
@@@ note
The `LogCapturing` utility @ref:[requires Logback dependency](logging.md#logback).
@@@
@scala[Mix `LogCapturing` trait into the ScalaTest]@java[Add a `LogCapturing` `@Rule` in the JUnit test] like this:
Scala
: @@snip [ScalaTestIntegrationExampleSpec.scala](/akka-actor-testkit-typed/src/test/scala/docs/akka/actor/testkit/typed/scaladsl/ScalaTestIntegrationExampleSpec.scala) { #log-capturing }
Java
: @@snip [LogCapturingExampleTest.java](/akka-actor-testkit-typed/src/test/java/jdocs/akka/actor/testkit/typed/javadsl/LogCapturingExampleTest.java) { #log-capturing }
Then you also need to configure the `CapturingAppender` and `CapturingAppenderDelegate` in
`src/test/resources/logback-test.xml`:
@@snip [logback-test.xml](/akka-actor-typed-tests/src/test/resources/logback-doc-test.xml)
## Synchronous behavior testing
The `BehaviorTestKit` provides a very nice way of unit testing a `Behavior` in a deterministic way, but it has

View file

@ -4,9 +4,8 @@
<statusListener class="ch.qos.logback.core.status.NopStatusListener" />
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>DEBUG</level>
<onMatch>DENY</onMatch>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<encoder>
<pattern>%date{ISO8601} %-5level %logger %X{akkaSource} %X{sourceThread} - %msg%n</pattern>

View file

@ -4,9 +4,8 @@
<statusListener class="ch.qos.logback.core.status.NopStatusListener" />
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>DEBUG</level>
<onMatch>DENY</onMatch>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<encoder>
<pattern>%date{ISO8601} %-5level %logger %X{akkaSource} %X{sourceThread} { "persistenceId": "%X{persistenceId}", "persistencePhase": "%X{persistencePhase}" } - %msg%n</pattern>

View file

@ -24,6 +24,7 @@ object Dependencies {
val jacksonVersion = "2.9.9"
val jacksonDatabindVersion = "2.9.9.3"
val protobufJavaVersion = "3.9.1"
val logbackVersion = "1.2.3"
val scala212Version = "2.12.9"
val scala213Version = "2.13.0"
@ -98,7 +99,7 @@ object Dependencies {
val protobufRuntime = "com.google.protobuf" % "protobuf-java" % protobufJavaVersion
val logback = "ch.qos.logback" % "logback-classic" % "1.2.3" // EPL 1.0
val logback = "ch.qos.logback" % "logback-classic" % logbackVersion // EPL 1.0
object Docs {
val sprayJson = "io.spray" %% "spray-json" % "1.3.5" % "test"

View file

@ -39,7 +39,8 @@ object Paradox {
"fiddle.code.base_dir" -> (sourceDirectory in Test).value.getAbsolutePath,
"fiddle.akka.base_dir" -> (baseDirectory in ThisBuild).value.getAbsolutePath,
"aeron_version" -> Dependencies.aeronVersion,
"netty_version" -> Dependencies.nettyVersion))
"netty_version" -> Dependencies.nettyVersion,
"logback_version" -> Dependencies.logbackVersion))
val rootsSettings = Seq(
paradoxRoots := List(