This post is the second of a two parts series of articles on Stackable Traits
- Part-1: Error reporting design with Stackable Traits
- Part-2: Gathering Metrics with Stackable Actors
Stackable traits can be applied to actors as well. Specifically, we can use stackable actor traits to modify the behavior of the ‘receive’ method.
Part-2: Gathering Metrics
We like to gather the following metrics
-
time-in-mailbox: The time from the moment a message was enqueued into an actor’s mailbox until the moment it was dequeued for processing.
-
processing-time:How long did it take for the actor to process a message.
And to log when an actor starts handling a message and before it finishes.
Stackable Actors-based implementation
Say we have the following actor that we like to monitor:
abstract class MyActor extends Actor with StrictLogging {
override def receive: Receive = {
case message => logger.info("performing some work...")
}
}
Let’s start with the ‘time-in-mailbox’ metric. The simplest way to implement it is to take the time before the message was sent and calculate the time in the mailbox when the actor starting processing it. For the sake of example, we’ll assume that a message was created just before it was sent.
The message class that should be monitored is:
trait RecordableMessage extends RichMessage {
val dispatchTime: Long =
System.currentTimeMillis()
}
trait RichMessage {
val messageName: String
}
We initialized the time before the message was sent, and gave it a name, to be used as a tag for the metric.
Next, create the stackable trait for monitoring the actor on RecordableMessage
trait LatencyRecorderActor extends Actor with StrictLogging {
val actorName: String = this.getClass.getSimpleName
abstract override def receive: Receive = {
case recordableMessage: RecordableMessage =>
Monitor.record("time-in-mailbox", actorName, recordableMessage.messageName,
recordableMessage.dispatchTime)
val start = System.currentTimeMillis()
super.receive(recordableMessage)
Monitor.record("processing-time", actorName, recordableMessage.messageName, start)
case message => super.receive(message)
}
}
You might notice that
-
As discussed in Part-1, the modifier of the ‘receive’ method should be “abstract override”
-
We gather the metrics only on the ‘RecordableMessage’ message
-
For calculating ‘time-in-mailbox’, ‘dispatchTime’ is used
-
For calculating ‘processing-time’, we take time before invoking the action, then invoking the action, and record the ‘processing-time’ when it finished.
The LoggerActor is the following
trait LoggerActor extends Actor with StrictLogging {
abstract override def receive: Receive = {
case recordableMessage: RecordableMessage =>
logger.info(s"handling message: ${recordableMessage.messageName}")
super.receive(recordableMessage)
logger.info(s"done handling message: ${recordableMessage.messageName}")
case message => super.receive(message)
}
}
Lastly, mix these traits to a concrete MyActor class
class MyMonitoredActor extends MyActor with LatencyRecorderActor with LoggerActor
And we ended up with:
Try it out
Create a concrete RecordableMessage:
case class SomeRecordableMessage() extends RecordableMessage {
override val messageName: String =
this.getClass.getSimpleName
}
And send it to a MonitoredActor instance
val actorSystem = ActorSystem("system")
val myMonitoredActor = actorSystem.actorOf(Props[MyMonitoredActor])
myMonitoredActor ! SomeRecordableMessage()
Results with the following printed to the log:
handling message: SomeTriggerMessage
time-in-mailbox latency for message SomeTriggerMessage in actor MyMonitoredActor is 102
performing some work...
processing-time latency for message SomeTriggerMessage in actor MyMonitoredActor is 212
done handling message: SomeTriggerMessage
Wrapping up
Stackable traits pattern is a good choice when you need to ‘pipe’ actions or modify and redirect data for an action. Mix and stack traits to describe the state of the class and execute the actions are clean and flexible, and generally the Scala-functional way to do it.
The complete source code can be found in my github.