Akka中的日志不会依赖于特定的日志后端。默认情况下,日志消息会打印到标准输出STDOUT,但是你可以插入SLF4J日志记录器或者你自己的日志记录器。日志是异步执行的,以确保日志具有最小的性能开销。日志通常意味着IO和锁定,如果代码时异步执行的,那么IO和锁定就会减慢代码的操作。
如何记录日志
创建日志适配器,使用error、warning、info或debug方法,正如下面这个例子说明:
import akka.actor.*;
import akka.event.Logging;
import akka.event.LoggingAdapter;
import scala.Option;
class MyActor extends UntypedActor {
LoggingAdapter log = Logging.getLogger(getContext().system(), this);
@Override
public void preStart() {
log.debug("Starting");
}
@Override
public void preRestart(Throwable reason, Option<Object> message) {
log.error(reason, "Restarting due to [{}] when processing [{}]", reason.getMessage(),
message.isDefined() ? message.get() : "");
}
public void onReceive(Object message) {
if (message.equals("test")) {
log.info("Received test");
} else {
log.warning("Received unknown message: {}", message);
}
}
}
Logging.getLogger的第一个参数也可能是任意的日志总线(LoggingBus),尤其是system.eventStream();在示例里,actor system的地址包含在日志源的akkaSource表示(参见Logging Thread, Akka Source and Actor System in MDC),而在第二个示例里,这不是自动完成的。Logging.getLogger的第二个参数是日志信道的源。源对象被会转换为一个字符串,规则如下:
· 如果是Actor或ActorRef,就使用它的path
· 如果是字符串,就直接用
· 如果是Class,使用它的simpleName的近似值
· 所有其它的场景,使用它的Class的simpleName
日志消息可能包含参数占位符{},如果日志级别被使能,那么这个占位符就会被代替。给予更多的参数作为占位符将导致在输出到日志语句时发生警告(即同一行具有相同的严重性)。你可能需要传入一个Java数组,因为这唯一的替代参数中的元素都被独立对待:
finalObject[] args = new Object[] {"The", "brown","fox", "jumps", 42 };
system.log().debug("five parameters: {}, {}, {}, {}, {}", args);
日志源的Java类也包含在生成的LogEvent中。如果是简单的字符串,那么会用标记类akka.event.DummyClassForStringSources代替,以允许特殊的场景,例如SLF4J事件监听器。SLF4J事件监听器会使用字符串代替类名来查找要使用的日志实例。
死信的日志
默认情况下,发送给死信的消息按info级别记录。存在死信并不表明存在问题,但是也可能存在问题,因此默认情况下它们是记日志的。在几个消息之后,这个日志被关闭,避免洪泛日志。你可以完全禁止这个日志,来调整要记录多少死信。在系统关闭过程中,你有可能看到死信,由于推迟将actor邮箱中的消息发送给死信。在关闭过程中,你也可以关闭死信的日志:
akka {
log-dead-letters = 10
log-dead-letters-during-shutdown = on
}
为了进一步自定义日志或者对死信执行其它动作,你可以订阅事件流Event Stream。
附加的日志选项
Akka有一些非常低级别调试的配置选项,这对于开发人员来说是非常有意义的,而不是运行。使用下面的任意选项,你几乎肯定需要将日志设置为DEBUG:
akka {
loglevel = "DEBUG"
}
这个配置选项是非常好的,如果你想要知道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
}
如果你想要Actor处理的所有自动接收的消息非常详细的日志:
akka {
actor {
debug {
# enable DEBUG logging of all AutoReceiveMessages (Kill, PoisonPill et.c.)
autoreceive = on
}
}
}
如果你想要Actor所有生命周期变化(重启、死亡等)的非常详细的日志:
akka {
actor {
debug {
# enable DEBUG logging of actor lifecycle changes
lifecycle = on
}
}
}
如果你想要将未处理的消息记录为DEBUG:
akka {
actor {
debug {
# enable DEBUG logging of unhandled messages
unhandled = on
}
}
}
如果你想要继承自LoggingFSM的FSM Actor的所有事件、迁移、定时器的详细日志:
akka {
actor {
debug {
# enable DEBUG logging of all LoggingFSMs for events, transitions and timers
fsm = on
}
}
}
如果你想要监视ActorSystem.eventStream的订阅(订阅和取消订阅):
akka {
actor {
debug {
# enable DEBUG logging of subscription changes on the eventStream
event-stream = on
}
}
}
辅助的远程日志选项
如果你想要在DEBUG日志级别看到所有通过远程发送的消息:(这是作为传输层发送的日志,不是Actor)
akka {
remote {
# If this is "on", Akka will log all outbound messages at DEBUG level,
# if off then they are not logged
log-sent-messages = on
}
}
如果你想要在DEBUG日志级别看到所有通过远程接收的消息:(这是作为传输层接收的日志,不是任意的Actor)
akka {
remote {
# If this is "on", Akka will log all inbound messages at DEBUG level,
# if off then they are not logged
log-received-messages = on
}
}
如果你想要在INFO日志级别看到消息负载大于指定字节限制的消息:
akka {
remote {
# Logging of message types with payload size in bytes larger than
# this value. Maximum detected size per message type is logged once,
# with an increase threshold of 10%.
# By default this feature is turned off. Activate it by setting the property to
# a value in bytes, such as 1000b. Note that for all messages larger than this
# limit there will be extra performance and scalability cost.
log-frame-size-exceeding = 1000b
}
}
你也可以看看TestKit的日志选项:Tracing Actor Invocations.
关闭日志
为了关闭日志,你可以配置日志级别为OFF:
akka {
stdout-loglevel = "OFF"
loglevel = "OFF"
}
stdout-loglevel只影响系统的启动和关闭,将它设置为OFF,那么确保在系统启动和关闭时没有任何日志。
日志记录器
日志是通过事件总线异步执行的。Log事件有事件处理器Actor处理,它会按照日志事件发出的顺序接收日志事件。
注意:事件处理器Actor没有有界有限,并且运行在默认的分发器上。这意味着记录大量日志数据可能会非常影响应用性能。从某种程度上说,使用异步日志后端会减少这个影响。(参见直接使用SLF4J API)
你可以配置在系统启动创建哪个事件处理器,监听日志事件。这是使用配置中的loggers元素实现的。这里也可以定义日志级别。基于日志源的更细粒度的过滤可以通过自定义LoggingFilter实现,这可以在配置属性中的logging-filter定义:
akka {
# Loggers to register at boot time (akka.event.Logging$DefaultLogger logs
# to STDOUT)
loggers = ["akka.event.Logging$DefaultLogger"]
# Options: OFF, ERROR, WARNING, INFO, DEBUG
loglevel = "DEBUG"
}
默认的日志记录器是记录到STDOUT,默认就被注册了。这不是打算用于生产环境的。在akka-slf4j模块中有可用的SLF4J日志记录器。
创建监听器的例子:
import akka.actor.*;
import akka.event.Logging;
import akka.event.LoggingAdapter;
import akka.event.Logging.InitializeLogger;
import akka.event.Logging.Error;
import akka.event.Logging.Warning;
import akka.event.Logging.Info;
import akka.event.Logging.Debug;
class MyEventListener extends UntypedActor {
public void onReceive(Object message) {
if (message instanceof InitializeLogger) {
getSender().tell(Logging.loggerInitialized(), getSelf());
} else if (message instanceof Error) {
// ...
} else if (message instanceof Warning) {
// ...
} else if (message instanceof Info) {
// ...
} else if (message instanceof Debug) {
// ...
}
}
}
在启动和关闭过程中记录日志到标准输出
虽然配置了日志记录器,但是当actor系统正在启动或者关闭时,日志记录器并不会被使用。取而代之的是,日志消息打印到标准输出System.out。这个标准输出的日志记录器的日志级别是WARNING,通过设置akka.stdout-loglevel=OFF,它会完全沉默的。
SLF4J
Akka提供了SL4FJ的日志记录器。这个模块位于akka-slf4j.jar。它有一个依赖slf4j-api jar。在运行时,你也需要SLF4J后端,我们推荐Logback:
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.1.3</version>
</dependency>
你需要在配置的loggers元素使能Slf4jLogger。这里你也可以定义事件总线的日志级别。更加细粒度的日志级别可以在SLF4J后端的配置中定义,例如logback.xml。你应该在配置属性logging-filter里定义akka.event.slf4j.Slf4jLoggingFilter。在它们被发布到事件总线之前,它将使用后端配置过滤日志事件(例如logback.xml)。
警告
如果你设置日志级别比"DEBUG"更高,任何DEBUG事件都将在源被过滤,决不会达到日志后端,不管后端是如何配置的:
akka {
loggers = ["akka.event.slf4j.Slf4jLogger"]
loglevel = "DEBUG"
logging-filter = "akka.event.slf4j.Slf4jLoggingFilter"
}
时间戳是在事件处理器中加上的,而不是实际记录日志时。
当创建LoggingAdapter,每一个事件选择的SLF4J日志记录器都是基于特定日志源的Class选择的,除非直接给出了字符串(即,第一种情况使用LoggerFactory.getLogger(Class c),第二种情况使用LoggerFactory.getLogger(String s)。
注意:如果创建LoggingAdapter时将ActorSystem给了日志记录器工厂,actor系统的名字将输出到String日志源。如果不想要这个,按如下方式给定LoggingBus:
final LoggingAdapter log = Logging.getLogger(system.eventStream(), "my.string");
直接使用SLF4J API
如果在应用中直接使用SLF4J API,当底层架构写日志语句时,要记住日志操作将会阻塞。
通过配置实用非阻塞的输出源的日志实现可以避免这个问题。Logback提供了AsyncAppender类来完成这个功能。它还有一个特性,就是如果日志负载太重,它也会丢弃INFO和DEBUG消息。
MDC中的日志线程、Akka Source和Actor System
由于记录日志是异步的,执行日志记录的线程被映射诊断上下文(Mapped Diagnostic Context MDC)以属性名sourceThread捕获。Logback中线程名可在pattern配置中用%X{sourceThread}获取:
<appendername="STDOUT"class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%date{ISO8601} %-5level %logger{36} %X{sourceThread} - %msg%n</pattern>
</encoder>
</appender>
注意:为了让sourceThread MDC值在日志中保持一致性,在应用的非Akka部分使用sourceThread MDC值可能是个好主意。
另一个有用的工具是当在actor内实例化日志记录器时,Akka可以捕获actor的地址,意思是说完整的实例标识对于相关的日志消息来说都是可用的,例如router成员。这个信息可在MDC中使用属性名akkaSource获取到:
<appendername="STDOUT"class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%date{ISO8601} %-5level %logger{36} %X{akkaSource} - %msg%n</pattern>
</encoder>
</appender>
最后,执行日志的actor系统也可以再MDC中用属性名sourceActorSystem 获取到:
<appendername="STDOUT"class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%date{ISO8601} %-5level %logger{36} %X{sourceActorSystem} - %msg%n</pattern>
</encoder>
</appender>
想要了解这个属性包含哪些细节,请参考How to Log。
MDC日志输出中的更加精确的时间戳
令人诧异的是,Akka日志是异步的,这意味着日志条目的时间戳是调用底层日志实现的时间。如果你想要更加精确的输出时间戳,使用MDC的属性akkaTimestamp:
<appendername="STDOUT"class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%X{akkaTimestamp} %-5level %logger{36} %X{akkaSource} - %msg%n</pattern>
</encoder>
</appender>
由应用定义MDC值
Slf4j中一个有用的特性就是MDC,Akka有让应用指定自定义值的方式,你只需要获取特定的LoggingAdapter:DiagnosticLoggingAdapter。为了获得DiagnosticLoggingAdapter,你将使用接收UntypedActor类型作为logSource的工长:
// Within your UntypedActor
final DiagnosticLoggingAdapter log = Logging.getLogger(this);
一旦你获取到这个日志记录器,你只需要在记录任何日志之前添加自定义的值即可。在输出日志之前,这种方式会将这些值会正确地放入SLF4J MDC中;输出日志后再移除。
注意:在actor中清理(移除)应该再最后做,否则下一个消息将会以同样的MDC值记录日志。使用log.clearMDC()。
import akka.actor.UntypedActor;
import akka.event.DiagnosticLoggingAdapter;
import akka.event.Logging;
import java.util.HashMap;
import java.util.Map;
class MdcActor extends UntypedActor {
final DiagnosticLoggingAdapter log = Logging.getLogger(this);
@Override
publicvoid onReceive(Object message) {
Map<String, Object> mdc;
mdc = new HashMap<String, Object>();
mdc.put("requestId", 1234);
mdc.put("visitorId", 5678);
log.setMDC(mdc);
log.info("Starting new request");
log.clearMDC();
}
}
现在,这些值在MDC都是可用的,你可以再布局模式中使用它们:
<appendername="STDOUT"class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>
%-5level %logger{36} [req: %X{requestId}, visitor: %X{visitorId}] - %msg%n
</pattern>
</encoder>
</appender>
使用标记Marker
一些日志库允许在日志消息中附加所谓的“标记”,除了MDC数据。这些标记用于过滤极少的特定事件,例如你可能标记标记日志,检测到了一些恶意活动,并将它们标记为ECURITY标签。在你的输出源配置中,让这些标记立即触发邮件或者其他的通知。
当获取LoggingAdapter时,Marker可通过Logging.withMarker获取。第一个传入的参数被传递给所有的日志调用,然后是akka.event.LogMarker。
akka-slf4j模块中提供的slf4j桥会自动获取这个marker值,对于SLF4J是可用的。例如你可以这样使用:
<pattern>%date{ISO8601} [%marker][%level] [%msg]%n</pattern>
更加高级的示例模式 (包含大多数Akka新增的信息):
<pattern>%date{ISO8601} level=[%level] marker=[%marker] logger=[%logger] akkaSource=[%X{akkaSource}] sourceActorSystem=[%X{sourceActorSystem}] sourceThread=[%X{sourceThread}] mdc=[ticket-#%X{ticketNumber}: %X{ticketDesc}] - msg=[%msg]%n----%n</pattern>