This library provides Akka specific functionality for integrating Echopraxia structured logging into Akka's Scala API. It provides pre-built default mappings for common Akka components such as paths, actors, and so on, and extends Akka Actors and Akka Streams with type enrichment for logging.
This README assumes that you have akka-slf4j
set up and have logging configured appropriately in Akka, and have the Scala API in scope.
Akka Actor support can be imported using
libraryDependencies += "com.tersesystems.echopraxia.plusakka" %% "akka-actor" % plusAkkaVersion
There is an AkkaFieldBuilder
trait that provides field builder mappings for common class akka.actor.Address
, akka.actor.ActorRef
, etc.
The DefaultAkkaFieldBuilder
implementation fills out those mappings, and there's a DefaultAkkaFieldBuilderProvider
that exposes that field builder.
package akka.echopraxia.actor
trait DefaultAkkaFieldBuilderProvider extends AkkaFieldBuilderProvider {
override type FieldBuilderType = DefaultAkkaFieldBuilder.type
override protected def fieldBuilder: FieldBuilderType = DefaultAkkaFieldBuilder
}
The trait ActorLogging
provides a logger, and wants a AkkaFieldBuilderProvider
trait to be mixed in:
package akka.echopraxia.actor
trait ActorLogging {
this: Actor with AkkaFieldBuilderProvider =>
protected val log: Logger[FieldBuilderType] = ...
}
This means that at the end of it, you can add ActorLogging with DefaultAkkaFieldBuilderProvider
to your actor, and it will log appropriately.
import akka.echopraxia.actor._
class MyActor extends Actor with ActorLogging with DefaultAkkaFieldBuilderProvider {
override def preStart() = {
log.debug("Starting")
}
override def preRestart(reason: Throwable, message: Option[Any]): Unit = {
log.error("Restarting due to [{}] when processing [{}]", fb => fb.list(
fb.exception(reason),
fb.string("message" -> message.toString),
fb.keyValue("self" -> self.path)
))
}
def receive = {
case "test" => log.info("Received test")
case x => log.warn("Received unknown message: {}", _.string("x" -> x.toString))
}
}
You can also use LoggingReceive
as usual -- it takes EchopraxiaLoggingAdapter
as input, which is available from ActorLogging
:
def receive = LoggingReceive {
case "test" => log.info("Received test")
case x => log.warn("Received unknown message: {}", _.string("x" -> x.toString))
}
NOTE: as in the original LoggingReceive
, the logging messages will not be enabled unless akka.actor.debug.receive=true
is set in configuration.
Echopraxia support for Akka Typed can be imported
libraryDependencies += "com.tersesystems.echopraxia.plusakka" %% "akka-actor-typed" % plusAkkaVersion
Echopraxia logging for Akka Typed is similar to typed logging.
There is an AkkaTypedFieldBuilder
trait that requires ToValue
for Akka Typed classes like akka.actor.typed.ActorRef[T]
, and a DefaultAkkaTypedFieldBuilder
trait and companion object that provides the default mappings.
Typically, you'll create your own field builder by extending these traits. For example, using the hello world example from the docs:
trait HelloWorldFieldBuilder extends DefaultAkkaTypedFieldBuilder {
implicit val greetToValue: ToObjectValue[Greet] = { greet: Greet =>
ToObjectValue(
keyValue("whom", greet.whom),
keyValue("replyTo", greet.replyTo)
)
}
// ...
}
object HelloWorldFieldBuilder extends HelloWorldFieldBuilder
Akka Typed incorporates the context through behaviors, using a context. This context is added to context.log
as MDC, but you can add it directly as context to the logger using withActorContext
, which is a type enrichment method you can add with import akka.echopraxia.actor.typed.Implicits._
:
package example
import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.{ActorSystem, Behavior}
import akka.echopraxia.actor.typed.DefaultAkkaTypedFieldBuilder
import akka.echopraxia.actor.typed.Implicits._
import com.tersesystems.echopraxia.plusscala.LoggerFactory
import scala.concurrent.duration._
object Main {
trait Command
case object Tick extends Command
case class Echo(message: String) extends Command
def main(args: Array[String]): Unit = {
val system = ActorSystem(MyActor(), "hello")
}
object MyActor {
def apply(): Behavior[Tick.type] = Behaviors.setup { context =>
Behaviors.withTimers { timers =>
val echo = context.spawn(EchoActor(), "echo")
timers.startTimerWithFixedDelay(Tick, 1.seconds)
Behaviors.receiveMessage {
case Tick =>
echo ! Echo(java.time.Instant.now().toString)
Behaviors.same
}
}
}
}
trait MyFieldBuilder extends DefaultAkkaTypedFieldBuilder {
implicit val commandToValue: ToValue[Command] = cmd => ToValue(cmd.toString)
}
object MyFieldBuilder extends MyFieldBuilder
object EchoActor {
def apply(): Behavior[Echo] = Behaviors.setup { context =>
val frozenContext = MyFieldBuilder.keyValue("context" -> context)
val logger = LoggerFactory.getLogger
.withFieldBuilder(MyFieldBuilder)
.withActorContext(context)
Behaviors.receiveMessage { echo =>
logger.info("echoActor: {}", _.keyValue("echo", echo))
Behaviors.same
}
}
}
}
Type enrichment also adds logger.debugMessages
, which can be used in a similar fashion to Behaviors.logMessages
logging.
object GreeterMain {
import akka.echopraxia.actor.typed.Implicits._
final case class SayHello(name: String)
object MyFieldBuilder extends DefaultAkkaTypedFieldBuilder {
implicit val sayHelloToValue: ToObjectValue[SayHello] = hello =>
ToObjectValue(
keyValue("@type" -> "SayHello"),
keyValue("name" -> hello.name)
)
// ...
}
def apply(): Behavior[SayHello] =
Behaviors.setup { context =>
val greeter = context.spawn(Greeter(), "greeter")
// Add ActorContext as field from setup
val logger = LoggerFactory.getLogger.withFieldBuilder(MyFieldBuilder).withActorContext(context)
// Then log SayHello messages
logger.debugMessages[SayHello] {
Behaviors.receiveMessage { message =>
val replyTo = context.spawn(GreeterBot(max = 3), message.name)
greeter ! Greeter.Greet(message.name, replyTo)
Behaviors.same
}
}
}
}
If you would rather use context.log
, you can use the "Direct SLF4J API" from Echopraxia to pass through conditions and fields through markers, and arguments can be passed through the field builder interface as usual.
object HelloWorld extends MyFieldBuilder {
def apply(): Behavior[Greet] = {
Behaviors.receive { (context, message) =>
// keyValue(name, ToValue) is provided by field builder API
// context.log will set MDC values when called for the thread.
context.log.info("Received message: {}", keyValue("foo", message))
message.replyTo ! Greeted(message.whom, context.self)
Behaviors.same
}
}
}
Note: Echopraxia filters will not go through the direct API, so if you have "global" conditions or fields that you want to apply to all loggers, they will need to be added directly to context.log
, or you'll need to add a Logback filter or turbo filter.
Echopraxia Akka Streams support can be installed using the following path:
libraryDependencies += "com.tersesystems.echopraxia.plusakka" %% "akka-stream" % plusAkkaVersion
Akka Streams comes with a number of components, which can be tedious to map out. The DefaultAkkaStreamFieldBuilder
singleton object comes with support for Source
, Sink
, Flow
, Inlet
, Outlet
and many more Akka Streams classes,
import akka.echopraxia.stream.DefaultAkkaStreamFieldBuilder
trait MyFieldBuilder extends DefaultAkkaStreamFieldBuilder {
// Add your own mappings
}
object MyFieldBuilder extends MyFieldBuilder
val logger = LoggerFactory.getLogger.withFieldBuilder(MyFieldBuilder)
There are two main reasons why Akka Streams should always incorporate logging of some sort in a source or flow.
The first reason is that by default, Akka Streams will swallow exceptions and not log them. As such, you need to have a log
or recover
op to make the exception visible.
The second reason is that logging is the best way to see what's happening in a stream. From the blog article, it's much better to log in each stage and turn on debugging.
You can't use a LoggingAdapter
here at all, because LoggingAdapter
throws away the original arguments and only logs a string template -- so logstash structuredArguments won't translate well, and you can't get JSON into the underlying logger.
Instead, you will need an EchopraxiaLoggingAdapter
in implicit scope, with Implicits._
providing the type enrichment needed for the logging operations:
import akka.echopraxia.stream.Implicits._
import akka.echopraxia.actor.EchopraxiaLoggingAdapter
implicit val loggingAdapter = EchopraxiaLoggingAdapter(this.getClass, MyFieldBuilder)
To log with structured logging, you'll call elog
, add conditions and fields as appropriate, and then log at the appropriate level:
val s = Source(1 to 4)
.elog
.info("before", (fb, el) => fb.keyValue("elem", el))
Note that the info
call takes a name indicating the stage, and also takes a function with the field builder and element, returning a Field
. You can elide this with _.keyValue("elem", _)
, i.e.
val s = Source(1 to 4)
.elog
.debug("before", _.keyValue("elem", _))
You can add context fields and conditions as appropriate to the stage in a fluent manner:
val s = Source(1 to 4)
.elog
.withCondition(condition)
.withFields(fb => fb.keyValue("foo", "bar"))
.info("before", (fb, el) => fb.keyValue("elem", el))
Logging in a flow uses the same API.
val f: Int => String = _.toString
Flow.fromFunction(f).elog.debug("name", _.keyValue("elem", _))
Logging in Akka Streams using the Java API is more involved, because type enrichment is not an option. If you want to use structured logging in the Java API with Echopraxia, you will need to use
logWithMarker with FieldMarker
and ConditionMarker
directly to bridge the gap.