aboutsummaryrefslogtreecommitdiff
path: root/kamon-akka/src
diff options
context:
space:
mode:
Diffstat (limited to 'kamon-akka/src')
-rw-r--r--kamon-akka/src/main/resources/META-INF/aop.xml34
-rw-r--r--kamon-akka/src/main/resources/reference.conf30
-rw-r--r--kamon-akka/src/main/scala/kamon/akka/ActorMetrics.scala41
-rw-r--r--kamon-akka/src/main/scala/kamon/akka/AkkaExtension.scala34
-rw-r--r--kamon-akka/src/main/scala/kamon/akka/DispatcherMetrics.scala86
-rw-r--r--kamon-akka/src/main/scala/kamon/akka/RouterMetrics.scala40
-rw-r--r--kamon-akka/src/main/scala/kamon/akka/instrumentation/ActorCellInstrumentation.scala212
-rw-r--r--kamon-akka/src/main/scala/kamon/akka/instrumentation/ActorLoggingInstrumentation.scala50
-rw-r--r--kamon-akka/src/main/scala/kamon/akka/instrumentation/ActorSystemMessageInstrumentation.scala80
-rw-r--r--kamon-akka/src/main/scala/kamon/akka/instrumentation/AskPatternInstrumentation.scala92
-rw-r--r--kamon-akka/src/main/scala/kamon/akka/instrumentation/DispatcherInstrumentation.scala169
-rw-r--r--kamon-akka/src/test/scala/kamon/akka/ActorMetricsSpec.scala215
-rw-r--r--kamon-akka/src/test/scala/kamon/akka/DispatcherMetricsSpec.scala207
-rw-r--r--kamon-akka/src/test/scala/kamon/akka/RouterMetricsSpec.scala184
-rw-r--r--kamon-akka/src/test/scala/kamon/akka/instrumentation/ActorCellInstrumentationSpec.scala87
-rw-r--r--kamon-akka/src/test/scala/kamon/akka/instrumentation/ActorLoggingInstrumentationSpec.scala74
-rw-r--r--kamon-akka/src/test/scala/kamon/akka/instrumentation/ActorSystemMessageInstrumentationSpec.scala196
-rw-r--r--kamon-akka/src/test/scala/kamon/akka/instrumentation/AskPatternInstrumentationSpec.scala137
18 files changed, 1968 insertions, 0 deletions
diff --git a/kamon-akka/src/main/resources/META-INF/aop.xml b/kamon-akka/src/main/resources/META-INF/aop.xml
new file mode 100644
index 00000000..46e63f91
--- /dev/null
+++ b/kamon-akka/src/main/resources/META-INF/aop.xml
@@ -0,0 +1,34 @@
+<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "http://www.eclipse.org/aspectj/dtd/aspectj.dtd">
+
+<aspectj>
+ <aspects>
+
+ <!-- Actors -->
+ <aspect name="akka.kamon.instrumentation.TraceContextIntoRepointableActorRefMixin"/>
+ <aspect name="akka.kamon.instrumentation.TraceContextIntoSystemMessageMixin"/>
+ <aspect name="akka.kamon.instrumentation.ActorSystemMessageInstrumentation"/>
+ <aspect name="akka.kamon.instrumentation.TraceContextIntoEnvelopeMixin"/>
+ <aspect name="akka.kamon.instrumentation.MetricsIntoActorCellsMixin"/>
+ <aspect name="akka.kamon.instrumentation.ActorCellInstrumentation"/>
+ <aspect name="akka.kamon.instrumentation.RoutedActorCellInstrumentation"/>
+ <aspect name="akka.kamon.instrumentation.ActorLoggingInstrumentation"/>
+
+ <!-- Dispatchers -->
+ <aspect name="akka.kamon.instrumentation.DispatcherInstrumentation"/>
+ <aspect name="akka.kamon.instrumentation.DispatcherMetricCollectionInfoIntoDispatcherMixin"/>
+
+ <!-- Patterns -->
+ <aspect name="akka.kamon.instrumentation.AskPatternInstrumentation"/>
+ </aspects>
+
+ <weaver>
+ <include within="akka..*"/>
+
+ <!-- For some weird reason ByteString produces a java.lang.VerifyError after going through the weaver. -->
+ <exclude within="akka.util.ByteString"/>
+
+ <!-- Exclude CallingThreadDispatcher, is only for test purposes -->
+ <exclude within="akka.testkit.CallingThreadDispatcher"/>
+ </weaver>
+
+</aspectj> \ No newline at end of file
diff --git a/kamon-akka/src/main/resources/reference.conf b/kamon-akka/src/main/resources/reference.conf
new file mode 100644
index 00000000..cc2b6060
--- /dev/null
+++ b/kamon-akka/src/main/resources/reference.conf
@@ -0,0 +1,30 @@
+# ================================== #
+# Kamon-Akka Reference Configuration #
+# ================================== #
+
+kamon {
+ akka {
+ # If ask-pattern-timeout-warning is enabled, a WARN level log message will be generated if a future generated by the `ask`
+ # pattern fails with a `AskTimeoutException` and the log message will contain information depending of the strategy selected.
+ # strategies:
+ # - off: nothing to do.
+ # - lightweight: logs the warning when a timeout is reached using org.aspectj.lang.reflect.SourceLocation.
+ # - heavyweight: logs the warning when a timeout is reached using a stack trace captured at the moment the future was created.
+ ask-pattern-timeout-warning = off
+
+ }
+
+ metric.filters {
+ actor.includes = []
+ actor.excludes = [ "", "user", "system**", "user/IO-**" ]
+ }
+
+
+ modules {
+ kamon-akka {
+ auto-start = yes
+ requires-aspectj = yes
+ extension-id = "kamon.akka.Akka"
+ }
+ }
+} \ No newline at end of file
diff --git a/kamon-akka/src/main/scala/kamon/akka/ActorMetrics.scala b/kamon-akka/src/main/scala/kamon/akka/ActorMetrics.scala
new file mode 100644
index 00000000..c99df586
--- /dev/null
+++ b/kamon-akka/src/main/scala/kamon/akka/ActorMetrics.scala
@@ -0,0 +1,41 @@
+/*
+ * =========================================================================================
+ * Copyright © 2013-2014 the kamon project <http://kamon.io/>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific language governing permissions
+ * and limitations under the License.
+ * =========================================================================================
+ */
+
+package kamon.akka
+
+import kamon.metric.{ EntityRecorderFactory, GenericEntityRecorder }
+import kamon.metric.instrument.{ Time, InstrumentFactory }
+
+/**
+ * Entity recorder for Akka Actors. The metrics being tracked are:
+ *
+ * - time-in-mailbox: Time spent from the instant when a message is enqueued in a actor's mailbox to the instant when
+ * that message is dequeued for processing.
+ * - processing-time: Time taken for the actor to process the receive function.
+ * - mailbox-size: Size of the actor's mailbox.
+ * - errors: Number or errors seen by the actor's supervision mechanism.
+ */
+class ActorMetrics(instrumentFactory: InstrumentFactory) extends GenericEntityRecorder(instrumentFactory) {
+ val timeInMailbox = histogram("time-in-mailbox", Time.Nanoseconds)
+ val processingTime = histogram("processing-time", Time.Nanoseconds)
+ val mailboxSize = minMaxCounter("mailbox-size")
+ val errors = counter("errors")
+}
+
+object ActorMetrics extends EntityRecorderFactory[ActorMetrics] {
+ def category: String = "akka-actor"
+ def createRecorder(instrumentFactory: InstrumentFactory): ActorMetrics = new ActorMetrics(instrumentFactory)
+} \ No newline at end of file
diff --git a/kamon-akka/src/main/scala/kamon/akka/AkkaExtension.scala b/kamon-akka/src/main/scala/kamon/akka/AkkaExtension.scala
new file mode 100644
index 00000000..5b3d19d4
--- /dev/null
+++ b/kamon-akka/src/main/scala/kamon/akka/AkkaExtension.scala
@@ -0,0 +1,34 @@
+/*
+ * =========================================================================================
+ * Copyright © 2013-2014 the kamon project <http://kamon.io/>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific language governing permissions
+ * and limitations under the License.
+ * =========================================================================================
+ */
+
+package kamon.akka
+
+import _root_.akka.actor
+import _root_.akka.actor._
+import kamon._
+
+class AkkaExtension(system: ExtendedActorSystem) extends Kamon.Extension {
+ val config = system.settings.config.getConfig("kamon.akka")
+ val askPatternTimeoutWarning = config.getString("ask-pattern-timeout-warning")
+ val dispatcher = system.dispatcher
+}
+
+object Akka extends ExtensionId[AkkaExtension] with ExtensionIdProvider {
+ def lookup(): ExtensionId[_ <: actor.Extension] = Akka
+ def createExtension(system: ExtendedActorSystem): AkkaExtension = new AkkaExtension(system)
+
+}
+
diff --git a/kamon-akka/src/main/scala/kamon/akka/DispatcherMetrics.scala b/kamon-akka/src/main/scala/kamon/akka/DispatcherMetrics.scala
new file mode 100644
index 00000000..acf92e70
--- /dev/null
+++ b/kamon-akka/src/main/scala/kamon/akka/DispatcherMetrics.scala
@@ -0,0 +1,86 @@
+/*
+ * =========================================================================================
+ * Copyright © 2013-2014 the kamon project <http://kamon.io/>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific language governing permissions
+ * and limitations under the License.
+ * =========================================================================================
+ */
+
+package kamon.akka
+
+import java.util.concurrent.ThreadPoolExecutor
+
+import _root_.akka.dispatch.ForkJoinExecutorConfigurator.AkkaForkJoinPool
+import kamon.metric._
+import kamon.metric.instrument.{ DifferentialValueCollector, InstrumentFactory }
+
+class ForkJoinPoolDispatcherMetrics(fjp: AkkaForkJoinPool, instrumentFactory: InstrumentFactory) extends GenericEntityRecorder(instrumentFactory) {
+ val paralellism = minMaxCounter("parallelism")
+ paralellism.increment(fjp.getParallelism) // Steady value.
+
+ val poolSize = gauge("pool-size", () ⇒ {
+ fjp.getPoolSize.toLong
+ })
+
+ val activeThreads = gauge("active-threads", () ⇒ {
+ fjp.getActiveThreadCount.toLong
+ })
+
+ val runningThreads = gauge("running-threads", () ⇒ {
+ fjp.getRunningThreadCount.toLong
+ })
+
+ val queuedTaskCount = gauge("queued-task-count", () ⇒ {
+ fjp.getQueuedTaskCount
+ })
+}
+
+object ForkJoinPoolDispatcherMetrics {
+
+ def factory(fjp: AkkaForkJoinPool) = new EntityRecorderFactory[ForkJoinPoolDispatcherMetrics] {
+ def category: String = AkkaDispatcherMetrics.Category
+ def createRecorder(instrumentFactory: InstrumentFactory) = new ForkJoinPoolDispatcherMetrics(fjp, instrumentFactory)
+ }
+}
+
+class ThreadPoolExecutorDispatcherMetrics(tpe: ThreadPoolExecutor, instrumentFactory: InstrumentFactory) extends GenericEntityRecorder(instrumentFactory) {
+ val corePoolSize = gauge("core-pool-size", () ⇒ {
+ tpe.getCorePoolSize.toLong
+ })
+
+ val maxPoolSize = gauge("max-pool-size", () ⇒ {
+ tpe.getMaximumPoolSize.toLong
+ })
+
+ val poolSize = gauge("pool-size", () ⇒ {
+ tpe.getPoolSize.toLong
+ })
+
+ val activeThreads = gauge("active-threads", () ⇒ {
+ tpe.getActiveCount.toLong
+ })
+
+ val processedTasks = gauge("processed-tasks", DifferentialValueCollector(() ⇒ {
+ tpe.getTaskCount
+ }))
+}
+
+object ThreadPoolExecutorDispatcherMetrics {
+
+ def factory(tpe: ThreadPoolExecutor) = new EntityRecorderFactory[ThreadPoolExecutorDispatcherMetrics] {
+ def category: String = AkkaDispatcherMetrics.Category
+ def createRecorder(instrumentFactory: InstrumentFactory) = new ThreadPoolExecutorDispatcherMetrics(tpe, instrumentFactory)
+ }
+}
+
+object AkkaDispatcherMetrics {
+ val Category = "akka-dispatcher"
+}
diff --git a/kamon-akka/src/main/scala/kamon/akka/RouterMetrics.scala b/kamon-akka/src/main/scala/kamon/akka/RouterMetrics.scala
new file mode 100644
index 00000000..5c5bb05a
--- /dev/null
+++ b/kamon-akka/src/main/scala/kamon/akka/RouterMetrics.scala
@@ -0,0 +1,40 @@
+/*
+ * =========================================================================================
+ * Copyright © 2013-2014 the kamon project <http://kamon.io/>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific language governing permissions
+ * and limitations under the License.
+ * =========================================================================================
+ */
+package kamon.akka
+
+import kamon.metric._
+import kamon.metric.instrument.{ Time, InstrumentFactory }
+
+/**
+ * Entity recorder for Akka Routers. The metrics being tracked are:
+ *
+ * - routing-time: Time taken for the router to process the routing logic.
+ * - time-in-mailbox: Time spent from the instant when a message is enqueued in a actor's mailbox to the instant when
+ * that message is dequeued for processing.
+ * - processing-time: Time taken for the actor to process the receive function.
+ * - errors: Number or errors seen by the actor's supervision mechanism.
+ */
+class RouterMetrics(instrumentFactory: InstrumentFactory) extends GenericEntityRecorder(instrumentFactory) {
+ val routingTime = histogram("routing-time", Time.Nanoseconds)
+ val timeInMailbox = histogram("time-in-mailbox", Time.Nanoseconds)
+ val processingTime = histogram("processing-time", Time.Nanoseconds)
+ val errors = counter("errors")
+}
+
+object RouterMetrics extends EntityRecorderFactory[RouterMetrics] {
+ def category: String = "akka-router"
+ def createRecorder(instrumentFactory: InstrumentFactory): RouterMetrics = new RouterMetrics(instrumentFactory)
+} \ No newline at end of file
diff --git a/kamon-akka/src/main/scala/kamon/akka/instrumentation/ActorCellInstrumentation.scala b/kamon-akka/src/main/scala/kamon/akka/instrumentation/ActorCellInstrumentation.scala
new file mode 100644
index 00000000..7c722569
--- /dev/null
+++ b/kamon-akka/src/main/scala/kamon/akka/instrumentation/ActorCellInstrumentation.scala
@@ -0,0 +1,212 @@
+/*
+ * =========================================================================================
+ * Copyright © 2013-2014 the kamon project <http://kamon.io/>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific language governing permissions
+ * and limitations under the License.
+ * =========================================================================================
+ */
+
+package akka.kamon.instrumentation
+
+import akka.actor._
+import akka.dispatch.{ Envelope, MessageDispatcher }
+import akka.routing.RoutedActorCell
+import kamon.Kamon
+import kamon.akka.{ RouterMetrics, ActorMetrics }
+import kamon.metric.Entity
+import kamon.trace._
+import org.aspectj.lang.ProceedingJoinPoint
+import org.aspectj.lang.annotation._
+
+@Aspect
+class ActorCellInstrumentation {
+
+ @Pointcut("execution(akka.actor.ActorCell.new(..)) && this(cell) && args(system, ref, props, dispatcher, parent)")
+ def actorCellCreation(cell: ActorCell, system: ActorSystem, ref: ActorRef, props: Props, dispatcher: MessageDispatcher, parent: ActorRef): Unit = {}
+
+ @After("actorCellCreation(cell, system, ref, props, dispatcher, parent)")
+ def afterCreation(cell: ActorCell, system: ActorSystem, ref: ActorRef, props: Props, dispatcher: MessageDispatcher, parent: ActorRef): Unit = {
+ Kamon.metrics.register(ActorMetrics, ref.path.elements.mkString("/")).map { registration ⇒
+ val cellMetrics = cell.asInstanceOf[ActorCellMetrics]
+
+ cellMetrics.entity = registration.entity
+ cellMetrics.recorder = Some(registration.recorder)
+ }
+
+ }
+
+ @Pointcut("execution(* akka.actor.ActorCell.invoke(*)) && this(cell) && args(envelope)")
+ def invokingActorBehaviourAtActorCell(cell: ActorCell, envelope: Envelope) = {}
+
+ @Around("invokingActorBehaviourAtActorCell(cell, envelope)")
+ def aroundBehaviourInvoke(pjp: ProceedingJoinPoint, cell: ActorCell, envelope: Envelope): Any = {
+ val cellMetrics = cell.asInstanceOf[ActorCellMetrics]
+ val timestampBeforeProcessing = System.nanoTime()
+ val contextAndTimestamp = envelope.asInstanceOf[TimestampedTraceContextAware]
+
+ try {
+ TraceContext.withContext(contextAndTimestamp.traceContext) {
+ pjp.proceed()
+ }
+ } finally {
+ cellMetrics.recorder.map { am ⇒
+ val processingTime = System.nanoTime() - timestampBeforeProcessing
+ val timeInMailbox = timestampBeforeProcessing - contextAndTimestamp.captureNanoTime
+
+ am.processingTime.record(processingTime)
+ am.timeInMailbox.record(timeInMailbox)
+ am.mailboxSize.decrement()
+
+ // In case that this actor is behind a router, record the metrics for the router.
+ envelope.asInstanceOf[RouterAwareEnvelope].routerMetricsRecorder.map { rm ⇒
+ rm.processingTime.record(processingTime)
+ rm.timeInMailbox.record(timeInMailbox)
+ }
+ }
+ }
+ }
+
+ @Pointcut("execution(* akka.actor.ActorCell.sendMessage(*)) && this(cell) && args(envelope)")
+ def sendMessageInActorCell(cell: ActorCell, envelope: Envelope): Unit = {}
+
+ @After("sendMessageInActorCell(cell, envelope)")
+ def afterSendMessageInActorCell(cell: ActorCell, envelope: Envelope): Unit = {
+ val cellMetrics = cell.asInstanceOf[ActorCellMetrics]
+ cellMetrics.recorder.map(_.mailboxSize.increment())
+ }
+
+ @Pointcut("execution(* akka.actor.ActorCell.stop()) && this(cell)")
+ def actorStop(cell: ActorCell): Unit = {}
+
+ @After("actorStop(cell)")
+ def afterStop(cell: ActorCell): Unit = {
+ val cellMetrics = cell.asInstanceOf[ActorCellMetrics]
+ cellMetrics.recorder.map { _ ⇒
+ Kamon.metrics.unregister(cellMetrics.entity)
+ }
+
+ // The Stop can't be captured from the RoutedActorCell so we need to put this piece of cleanup here.
+ if (cell.isInstanceOf[RoutedActorCell]) {
+ val routedCellMetrics = cell.asInstanceOf[RoutedActorCellMetrics]
+ routedCellMetrics.routerRecorder.map { _ ⇒
+ Kamon.metrics.unregister(routedCellMetrics.routerEntity)
+ }
+ }
+ }
+
+ @Pointcut("execution(* akka.actor.ActorCell.handleInvokeFailure(..)) && this(cell)")
+ def actorInvokeFailure(cell: ActorCell): Unit = {}
+
+ @Before("actorInvokeFailure(cell)")
+ def beforeInvokeFailure(cell: ActorCell): Unit = {
+ val cellWithMetrics = cell.asInstanceOf[ActorCellMetrics]
+ cellWithMetrics.recorder.map(_.errors.increment())
+
+ // In case that this actor is behind a router, count the errors for the router as well.
+ val envelope = cell.currentMessage.asInstanceOf[RouterAwareEnvelope]
+ envelope.routerMetricsRecorder.map(_.errors.increment())
+ }
+}
+
+@Aspect
+class RoutedActorCellInstrumentation {
+
+ @Pointcut("execution(akka.routing.RoutedActorCell.new(..)) && this(cell) && args(system, ref, props, dispatcher, routeeProps, supervisor)")
+ def routedActorCellCreation(cell: RoutedActorCell, system: ActorSystem, ref: ActorRef, props: Props, dispatcher: MessageDispatcher, routeeProps: Props, supervisor: ActorRef): Unit = {}
+
+ @After("routedActorCellCreation(cell, system, ref, props, dispatcher, routeeProps, supervisor)")
+ def afterRoutedActorCellCreation(cell: RoutedActorCell, system: ActorSystem, ref: ActorRef, props: Props, dispatcher: MessageDispatcher, routeeProps: Props, supervisor: ActorRef): Unit = {
+ Kamon.metrics.register(RouterMetrics, ref.path.elements.mkString("/")).map { registration ⇒
+ val cellMetrics = cell.asInstanceOf[RoutedActorCellMetrics]
+
+ cellMetrics.routerEntity = registration.entity
+ cellMetrics.routerRecorder = Some(registration.recorder)
+ }
+ }
+
+ @Pointcut("execution(* akka.routing.RoutedActorCell.sendMessage(*)) && this(cell) && args(envelope)")
+ def sendMessageInRouterActorCell(cell: RoutedActorCell, envelope: Envelope) = {}
+
+ @Around("sendMessageInRouterActorCell(cell, envelope)")
+ def aroundSendMessageInRouterActorCell(pjp: ProceedingJoinPoint, cell: RoutedActorCell, envelope: Envelope): Any = {
+ val cellMetrics = cell.asInstanceOf[RoutedActorCellMetrics]
+ val timestampBeforeProcessing = System.nanoTime()
+ val contextAndTimestamp = envelope.asInstanceOf[TimestampedTraceContextAware]
+
+ try {
+ TraceContext.withContext(contextAndTimestamp.traceContext) {
+
+ // The router metrics recorder will only be picked up if the message is sent from a tracked router.
+ RouterAwareEnvelope.dynamicRouterMetricsRecorder.withValue(cellMetrics.routerRecorder) {
+ pjp.proceed()
+ }
+ }
+ } finally {
+ cellMetrics.routerRecorder map { routerRecorder ⇒
+ routerRecorder.routingTime.record(System.nanoTime() - timestampBeforeProcessing)
+ }
+ }
+ }
+}
+
+trait ActorCellMetrics {
+ var entity: Entity = _
+ var recorder: Option[ActorMetrics] = None
+}
+
+trait RoutedActorCellMetrics {
+ var routerEntity: Entity = _
+ var routerRecorder: Option[RouterMetrics] = None
+}
+
+trait RouterAwareEnvelope {
+ def routerMetricsRecorder: Option[RouterMetrics]
+}
+
+object RouterAwareEnvelope {
+ import scala.util.DynamicVariable
+ private[kamon] val dynamicRouterMetricsRecorder = new DynamicVariable[Option[RouterMetrics]](None)
+
+ def default: RouterAwareEnvelope = new RouterAwareEnvelope {
+ val routerMetricsRecorder: Option[RouterMetrics] = dynamicRouterMetricsRecorder.value
+ }
+}
+
+@Aspect
+class MetricsIntoActorCellsMixin {
+
+ @DeclareMixin("akka.actor.ActorCell")
+ def mixinActorCellMetricsToActorCell: ActorCellMetrics = new ActorCellMetrics {}
+
+ @DeclareMixin("akka.routing.RoutedActorCell")
+ def mixinActorCellMetricsToRoutedActorCell: RoutedActorCellMetrics = new RoutedActorCellMetrics {}
+
+}
+
+@Aspect
+class TraceContextIntoEnvelopeMixin {
+
+ @DeclareMixin("akka.dispatch.Envelope")
+ def mixinTraceContextAwareToEnvelope: TimestampedTraceContextAware = TimestampedTraceContextAware.default
+
+ @DeclareMixin("akka.dispatch.Envelope")
+ def mixinRouterAwareToEnvelope: RouterAwareEnvelope = RouterAwareEnvelope.default
+
+ @Pointcut("execution(akka.dispatch.Envelope.new(..)) && this(ctx)")
+ def envelopeCreation(ctx: TimestampedTraceContextAware): Unit = {}
+
+ @After("envelopeCreation(ctx)")
+ def afterEnvelopeCreation(ctx: TimestampedTraceContextAware with RouterAwareEnvelope): Unit = {
+ // Necessary to force the initialization of ContextAware at the moment of creation.
+ ctx.traceContext
+ ctx.routerMetricsRecorder
+ }
+} \ No newline at end of file
diff --git a/kamon-akka/src/main/scala/kamon/akka/instrumentation/ActorLoggingInstrumentation.scala b/kamon-akka/src/main/scala/kamon/akka/instrumentation/ActorLoggingInstrumentation.scala
new file mode 100644
index 00000000..dd998c6b
--- /dev/null
+++ b/kamon-akka/src/main/scala/kamon/akka/instrumentation/ActorLoggingInstrumentation.scala
@@ -0,0 +1,50 @@
+/*
+ * =========================================================================================
+ * Copyright © 2013-2014 the kamon project <http://kamon.io/>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific language governing permissions
+ * and limitations under the License.
+ * =========================================================================================
+ */
+
+package akka.kamon.instrumentation
+
+import kamon.trace.logging.MdcKeysSupport
+import kamon.trace.{ TraceContext, TraceContextAware }
+import org.aspectj.lang.ProceedingJoinPoint
+import org.aspectj.lang.annotation._
+
+@Aspect
+class ActorLoggingInstrumentation extends MdcKeysSupport {
+
+ @DeclareMixin("akka.event.Logging.LogEvent+")
+ def mixinTraceContextAwareToLogEvent: TraceContextAware = TraceContextAware.default
+
+ @Pointcut("execution(akka.event.Logging.LogEvent+.new(..)) && this(event)")
+ def logEventCreation(event: TraceContextAware): Unit = {}
+
+ @After("logEventCreation(event)")
+ def captureTraceContext(event: TraceContextAware): Unit = {
+ // Force initialization of TraceContextAware
+ event.traceContext
+ }
+
+ @Pointcut("execution(* akka.event.slf4j.Slf4jLogger.withMdc(..)) && args(logSource, logEvent, logStatement)")
+ def withMdcInvocation(logSource: String, logEvent: TraceContextAware, logStatement: () ⇒ _): Unit = {}
+
+ @Around("withMdcInvocation(logSource, logEvent, logStatement)")
+ def aroundWithMdcInvocation(pjp: ProceedingJoinPoint, logSource: String, logEvent: TraceContextAware, logStatement: () ⇒ _): Unit = {
+ TraceContext.withContext(logEvent.traceContext) {
+ withMdc {
+ pjp.proceed()
+ }
+ }
+ }
+}
diff --git a/kamon-akka/src/main/scala/kamon/akka/instrumentation/ActorSystemMessageInstrumentation.scala b/kamon-akka/src/main/scala/kamon/akka/instrumentation/ActorSystemMessageInstrumentation.scala
new file mode 100644
index 00000000..0cb4ef13
--- /dev/null
+++ b/kamon-akka/src/main/scala/kamon/akka/instrumentation/ActorSystemMessageInstrumentation.scala
@@ -0,0 +1,80 @@
+/*
+ * =========================================================================================
+ * Copyright © 2013-2014 the kamon project <http://kamon.io/>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific language governing permissions
+ * and limitations under the License.
+ * =========================================================================================
+ */
+
+package akka.kamon.instrumentation
+
+import akka.dispatch.sysmsg.EarliestFirstSystemMessageList
+import kamon.trace.{ TraceContext, TraceContextAware }
+import org.aspectj.lang.ProceedingJoinPoint
+import org.aspectj.lang.annotation._
+
+@Aspect
+class ActorSystemMessageInstrumentation {
+
+ @Pointcut("execution(* akka.actor.ActorCell.invokeAll$1(..)) && args(messages, *)")
+ def systemMessageProcessing(messages: EarliestFirstSystemMessageList): Unit = {}
+
+ @Around("systemMessageProcessing(messages)")
+ def aroundSystemMessageInvoke(pjp: ProceedingJoinPoint, messages: EarliestFirstSystemMessageList): Any = {
+ if (messages.nonEmpty) {
+ val ctx = messages.head.asInstanceOf[TraceContextAware].traceContext
+ TraceContext.withContext(ctx)(pjp.proceed())
+
+ } else pjp.proceed()
+ }
+}
+
+@Aspect
+class TraceContextIntoSystemMessageMixin {
+
+ @DeclareMixin("akka.dispatch.sysmsg.SystemMessage+")
+ def mixinTraceContextAwareToSystemMessage: TraceContextAware = TraceContextAware.default
+
+ @Pointcut("execution(akka.dispatch.sysmsg.SystemMessage+.new(..)) && this(ctx)")
+ def envelopeCreation(ctx: TraceContextAware): Unit = {}
+
+ @After("envelopeCreation(ctx)")
+ def afterEnvelopeCreation(ctx: TraceContextAware): Unit = {
+ // Necessary to force the initialization of ContextAware at the moment of creation.
+ ctx.traceContext
+ }
+}
+
+@Aspect
+class TraceContextIntoRepointableActorRefMixin {
+
+ @DeclareMixin("akka.actor.RepointableActorRef")
+ def mixinTraceContextAwareToRepointableActorRef: TraceContextAware = TraceContextAware.default
+
+ @Pointcut("execution(akka.actor.RepointableActorRef.new(..)) && this(ctx)")
+ def envelopeCreation(ctx: TraceContextAware): Unit = {}
+
+ @After("envelopeCreation(ctx)")
+ def afterEnvelopeCreation(ctx: TraceContextAware): Unit = {
+ // Necessary to force the initialization of ContextAware at the moment of creation.
+ ctx.traceContext
+ }
+
+ @Pointcut("execution(* akka.actor.RepointableActorRef.point(..)) && this(repointableActorRef)")
+ def repointableActorRefCreation(repointableActorRef: TraceContextAware): Unit = {}
+
+ @Around("repointableActorRefCreation(repointableActorRef)")
+ def afterRepointableActorRefCreation(pjp: ProceedingJoinPoint, repointableActorRef: TraceContextAware): Any = {
+ TraceContext.withContext(repointableActorRef.traceContext) {
+ pjp.proceed()
+ }
+ }
+} \ No newline at end of file
diff --git a/kamon-akka/src/main/scala/kamon/akka/instrumentation/AskPatternInstrumentation.scala b/kamon-akka/src/main/scala/kamon/akka/instrumentation/AskPatternInstrumentation.scala
new file mode 100644
index 00000000..e1dcdf32
--- /dev/null
+++ b/kamon-akka/src/main/scala/kamon/akka/instrumentation/AskPatternInstrumentation.scala
@@ -0,0 +1,92 @@
+/*
+ * =========================================================================================
+ * Copyright © 2013-2014 the kamon project <http://kamon.io/>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific language governing permissions
+ * and limitations under the License.
+ * =========================================================================================
+ */
+
+package akka.kamon.instrumentation
+
+import akka.util.Timeout
+import kamon.Kamon
+import kamon.akka.Akka
+import kamon.trace.{ TraceContext, EmptyTraceContext, TraceContextAware }
+import akka.actor.{ InternalActorRef, ActorSystem, ActorRef }
+import akka.event.Logging.Warning
+import akka.pattern.{ PromiseActorRef, AskTimeoutException }
+import org.aspectj.lang.ProceedingJoinPoint
+import org.aspectj.lang.annotation._
+import org.aspectj.lang.reflect.SourceLocation
+import scala.concurrent.Future
+import scala.compat.Platform.EOL
+import scala.concurrent.duration.FiniteDuration
+
+@Aspect
+class AskPatternInstrumentation {
+
+ import AskPatternInstrumentation._
+
+ @Pointcut("call(* akka.pattern.AskableActorRef$.$qmark$extension(..)) && args(actor, *, timeout)")
+ def askableActorRefAsk(actor: ActorRef, timeout: Timeout): Unit = {}
+
+ @Around("askableActorRefAsk(actor, timeout)")
+ def hookAskTimeoutWarning(pjp: ProceedingJoinPoint, actor: ActorRef, timeout: Timeout): AnyRef =
+ TraceContext.map { ctx ⇒
+ actor match {
+ // the AskPattern will only work for InternalActorRef's with these conditions.
+ case ref: InternalActorRef if !ref.isTerminated && timeout.duration.length > 0 ⇒
+ val akkaExtension = Kamon.extension(Akka)
+ val future = pjp.proceed().asInstanceOf[Future[AnyRef]]
+ val system = ref.provider.guardian.underlying.system
+
+ val handler = akkaExtension.askPatternTimeoutWarning match {
+ case "off" ⇒ None
+ case "lightweight" ⇒ Some(errorHandler(callInfo = Some(CallInfo(s"${actor.path.name} ?", pjp.getSourceLocation)))(system))
+ case "heavyweight" ⇒ Some(errorHandler(stack = Some(new StackTraceCaptureException))(system))
+ }
+
+ handler.map(future.onFailure(_)(akkaExtension.dispatcher))
+ future
+
+ case _ ⇒ pjp.proceed() //
+ }
+
+ } getOrElse (pjp.proceed())
+
+ def errorHandler(callInfo: Option[CallInfo] = None, stack: Option[StackTraceCaptureException] = None)(implicit system: ActorSystem): ErrorHandler = {
+ case e: AskTimeoutException ⇒
+ val message = {
+ if (stack.isDefined) stack.map(s ⇒ s.getStackTrace.drop(3).mkString("", EOL, EOL))
+ else callInfo.map(_.message)
+ }
+ publish(message)
+ }
+
+ def publish(message: Option[String])(implicit system: ActorSystem) = message map { msg ⇒
+ system.eventStream.publish(Warning("AskPatternTracing", classOf[AskPatternInstrumentation],
+ s"Timeout triggered for ask pattern registered at: $msg"))
+ }
+}
+
+object AskPatternInstrumentation {
+ type ErrorHandler = PartialFunction[Throwable, Unit]
+
+ class StackTraceCaptureException extends Throwable
+
+ case class CallInfo(name: String, sourceLocation: SourceLocation) {
+ def message: String = {
+ def locationInfo: String = Option(sourceLocation).map(location ⇒ s"${location.getFileName}:${location.getLine}").getOrElse("<unknown position>")
+ def line: String = s"$name @ $locationInfo"
+ s"$line"
+ }
+ }
+} \ No newline at end of file
diff --git a/kamon-akka/src/main/scala/kamon/akka/instrumentation/DispatcherInstrumentation.scala b/kamon-akka/src/main/scala/kamon/akka/instrumentation/DispatcherInstrumentation.scala
new file mode 100644
index 00000000..7b15c443
--- /dev/null
+++ b/kamon-akka/src/main/scala/kamon/akka/instrumentation/DispatcherInstrumentation.scala
@@ -0,0 +1,169 @@
+/*
+ * =========================================================================================
+ * Copyright © 2013-2014 the kamon project <http://kamon.io/>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific language governing permissions
+ * and limitations under the License.
+ * =========================================================================================
+ */
+
+package akka.kamon.instrumentation
+
+import java.util.concurrent.{ ExecutorService, ThreadPoolExecutor }
+
+import akka.actor.{ ActorSystem, ActorSystemImpl }
+import akka.dispatch.ForkJoinExecutorConfigurator.AkkaForkJoinPool
+import akka.dispatch._
+import akka.kamon.instrumentation.LookupDataAware.LookupData
+import kamon.Kamon
+import kamon.akka.{ AkkaDispatcherMetrics, ThreadPoolExecutorDispatcherMetrics, ForkJoinPoolDispatcherMetrics }
+import kamon.metric.Entity
+import org.aspectj.lang.ProceedingJoinPoint
+import org.aspectj.lang.annotation._
+
+@Aspect
+class DispatcherInstrumentation {
+
+ @Pointcut("execution(* akka.actor.ActorSystemImpl.start(..)) && this(system)")
+ def actorSystemInitialization(system: ActorSystemImpl): Unit = {}
+
+ @Before("actorSystemInitialization(system)")
+ def afterActorSystemInitialization(system: ActorSystemImpl): Unit = {
+ system.dispatchers.asInstanceOf[ActorSystemAware].actorSystem = system
+
+ // The default dispatcher for the actor system is looked up in the ActorSystemImpl's initialization code and we
+ // can't get the Metrics extension there since the ActorSystem is not yet fully constructed. To workaround that
+ // we are manually selecting and registering the default dispatcher with the Metrics extension. All other dispatchers
+ // will by registered by the instrumentation bellow.
+
+ // Yes, reflection sucks, but this piece of code is only executed once on ActorSystem's startup.
+ val defaultDispatcher = system.dispatcher
+ val executorServiceDelegateField = defaultDispatcher.getClass.getDeclaredField("executorServiceDelegate")
+ executorServiceDelegateField.setAccessible(true)
+
+ val lazyExecutorServiceDelegate = executorServiceDelegateField.get(defaultDispatcher)
+ val executorField = lazyExecutorServiceDelegate.getClass.getMethod("executor")
+ executorField.setAccessible(true)
+
+ val defaultDispatcherExecutor = executorField.invoke(lazyExecutorServiceDelegate).asInstanceOf[ExecutorService]
+ registerDispatcher(Dispatchers.DefaultDispatcherId, defaultDispatcherExecutor, system)
+ }
+
+ private def registerDispatcher(dispatcherName: String, executorService: ExecutorService, system: ActorSystem): Unit =
+ executorService match {
+ case fjp: AkkaForkJoinPool ⇒
+ Kamon.metrics.register(ForkJoinPoolDispatcherMetrics.factory(fjp), dispatcherName)
+
+ case tpe: ThreadPoolExecutor ⇒
+ Kamon.metrics.register(ThreadPoolExecutorDispatcherMetrics.factory(tpe), dispatcherName)
+
+ case others ⇒ // Currently not interested in other kinds of dispatchers.
+ }
+
+ @Pointcut("execution(* akka.dispatch.Dispatchers.lookup(..)) && this(dispatchers) && args(dispatcherName)")
+ def dispatchersLookup(dispatchers: ActorSystemAware, dispatcherName: String) = {}
+
+ @Around("dispatchersLookup(dispatchers, dispatcherName)")
+ def aroundDispatchersLookup(pjp: ProceedingJoinPoint, dispatchers: ActorSystemAware, dispatcherName: String): Any =
+ LookupDataAware.withLookupData(LookupData(dispatcherName, dispatchers.actorSystem)) {
+ pjp.proceed()
+ }
+
+ @Pointcut("initialization(akka.dispatch.ExecutorServiceFactory.new(..)) && target(factory)")
+ def executorServiceFactoryInitialization(factory: LookupDataAware): Unit = {}
+
+ @After("executorServiceFactoryInitialization(factory)")
+ def afterExecutorServiceFactoryInitialization(factory: LookupDataAware): Unit =
+ factory.lookupData = LookupDataAware.currentLookupData
+
+ @Pointcut("execution(* akka.dispatch.ExecutorServiceFactory+.createExecutorService()) && this(factory) && !cflow(execution(* akka.dispatch.Dispatcher.shutdown()))")
+ def createExecutorService(factory: LookupDataAware): Unit = {}
+
+ @AfterReturning(pointcut = "createExecutorService(factory)", returning = "executorService")
+ def afterCreateExecutorService(factory: LookupDataAware, executorService: ExecutorService): Unit = {
+ val lookupData = factory.lookupData
+
+ // lookupData.actorSystem will be null only during the first lookup of the default dispatcher during the
+ // ActorSystemImpl's initialization.
+ if (lookupData.actorSystem != null)
+ registerDispatcher(lookupData.dispatcherName, executorService, lookupData.actorSystem)
+ }
+
+ @Pointcut("initialization(akka.dispatch.Dispatcher.LazyExecutorServiceDelegate.new(..)) && this(lazyExecutor)")
+ def lazyExecutorInitialization(lazyExecutor: LookupDataAware): Unit = {}
+
+ @After("lazyExecutorInitialization(lazyExecutor)")
+ def afterLazyExecutorInitialization(lazyExecutor: LookupDataAware): Unit =
+ lazyExecutor.lookupData = LookupDataAware.currentLookupData
+
+ @Pointcut("execution(* akka.dispatch.Dispatcher.LazyExecutorServiceDelegate.copy()) && this(lazyExecutor)")
+ def lazyExecutorCopy(lazyExecutor: LookupDataAware): Unit = {}
+
+ @Around("lazyExecutorCopy(lazyExecutor)")
+ def aroundLazyExecutorCopy(pjp: ProceedingJoinPoint, lazyExecutor: LookupDataAware): Any =
+ LookupDataAware.withLookupData(lazyExecutor.lookupData) {
+ pjp.proceed()
+ }
+
+ @Pointcut("execution(* akka.dispatch.Dispatcher.LazyExecutorServiceDelegate.shutdown()) && this(lazyExecutor)")
+ def lazyExecutorShutdown(lazyExecutor: LookupDataAware): Unit = {}
+
+ @After("lazyExecutorShutdown(lazyExecutor)")
+ def afterLazyExecutorShutdown(lazyExecutor: LookupDataAware): Unit = {
+ import lazyExecutor.lookupData
+
+ if (lookupData.actorSystem != null)
+ Kamon.metrics.unregister(Entity(lookupData.dispatcherName, AkkaDispatcherMetrics.Category))
+ }
+
+}
+
+@Aspect
+class DispatcherMetricCollectionInfoIntoDispatcherMixin {
+
+ @DeclareMixin("akka.dispatch.Dispatchers")
+ def mixinActorSystemAwareToDispatchers: ActorSystemAware = ActorSystemAware()
+
+ @DeclareMixin("akka.dispatch.Dispatcher.LazyExecutorServiceDelegate")
+ def mixinLookupDataAwareToExecutors: LookupDataAware = LookupDataAware()
+
+ @DeclareMixin("akka.dispatch.ExecutorServiceFactory+")
+ def mixinActorSystemAwareToDispatcher: LookupDataAware = LookupDataAware()
+}
+
+trait ActorSystemAware {
+ @volatile var actorSystem: ActorSystem = _
+}
+
+object ActorSystemAware {
+ def apply(): ActorSystemAware = new ActorSystemAware {}
+}
+
+trait LookupDataAware {
+ @volatile var lookupData: LookupData = _
+}
+
+object LookupDataAware {
+ case class LookupData(dispatcherName: String, actorSystem: ActorSystem)
+
+ private val _currentDispatcherLookupData = new ThreadLocal[LookupData]
+
+ def apply() = new LookupDataAware {}
+
+ def currentLookupData: LookupData = _currentDispatcherLookupData.get()
+
+ def withLookupData[T](lookupData: LookupData)(thunk: ⇒ T): T = {
+ _currentDispatcherLookupData.set(lookupData)
+ val result = thunk
+ _currentDispatcherLookupData.remove()
+
+ result
+ }
+} \ No newline at end of file
diff --git a/kamon-akka/src/test/scala/kamon/akka/ActorMetricsSpec.scala b/kamon-akka/src/test/scala/kamon/akka/ActorMetricsSpec.scala
new file mode 100644
index 00000000..0d8d41e3
--- /dev/null
+++ b/kamon-akka/src/test/scala/kamon/akka/ActorMetricsSpec.scala
@@ -0,0 +1,215 @@
+/* =========================================================================================
+ * Copyright © 2013 the kamon project <http://kamon.io/>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific language governing permissions
+ * and limitations under the License.
+ * =========================================================================================
+ */
+
+package kamon.akka
+
+import java.nio.LongBuffer
+
+import akka.actor._
+import akka.testkit.TestProbe
+import com.typesafe.config.ConfigFactory
+import kamon.Kamon
+import kamon.akka.ActorMetricsTestActor._
+import kamon.metric.EntitySnapshot
+import kamon.metric.instrument.CollectionContext
+import kamon.testkit.BaseKamonSpec
+
+import scala.concurrent.duration._
+
+class ActorMetricsSpec extends BaseKamonSpec("actor-metrics-spec") {
+ override lazy val config =
+ ConfigFactory.parseString(
+ """
+ |kamon.metric {
+ | tick-interval = 1 hour
+ | default-collection-context-buffer-size = 10
+ |
+ | filters {
+ | akka-actor {
+ | includes = [ "user/tracked-*", "user/measuring-*", "user/clean-after-collect", "user/stop" ]
+ | excludes = [ "user/tracked-explicitly-excluded", "user/non-tracked-actor" ]
+ | }
+ | }
+ |
+ | instrument-settings {
+ | akka-actor.mailbox-size.refresh-interval = 1 hour
+ | }
+ |}
+ |
+ |akka.loglevel = OFF
+ |
+ """.stripMargin)
+
+ "the Kamon actor metrics" should {
+ "respect the configured include and exclude filters" in new ActorMetricsFixtures {
+ val trackedActor = createTestActor("tracked-actor")
+ actorMetricsRecorderOf(trackedActor) should not be empty
+
+ val nonTrackedActor = createTestActor("non-tracked-actor")
+ actorMetricsRecorderOf(nonTrackedActor) shouldBe empty
+
+ val trackedButExplicitlyExcluded = createTestActor("tracked-explicitly-excluded")
+ actorMetricsRecorderOf(trackedButExplicitlyExcluded) shouldBe empty
+ }
+
+ "reset all recording instruments after taking a snapshot" in new ActorMetricsFixtures {
+ val trackedActor = createTestActor("clean-after-collect")
+
+ for (_ ← 1 to 100) {
+ for (i ← 1 to 100) {
+ trackedActor ! Discard
+ }
+ trackedActor ! Fail
+ trackedActor ! Ping
+ expectMsg(Pong)
+
+ val firstSnapshot = collectMetricsOf(trackedActor).get
+ firstSnapshot.counter("errors").get.count should be(1L)
+ firstSnapshot.minMaxCounter("mailbox-size").get.numberOfMeasurements should be > 0L
+ firstSnapshot.histogram("processing-time").get.numberOfMeasurements should be(102L) // 102 examples
+ firstSnapshot.histogram("time-in-mailbox").get.numberOfMeasurements should be(102L) // 102 examples
+
+ val secondSnapshot = collectMetricsOf(trackedActor).get // Ensure that the recorders are clean
+ secondSnapshot.counter("errors").get.count should be(0L)
+ secondSnapshot.minMaxCounter("mailbox-size").get.numberOfMeasurements should be(3L) // min, max and current
+ secondSnapshot.histogram("processing-time").get.numberOfMeasurements should be(0L)
+ secondSnapshot.histogram("time-in-mailbox").get.numberOfMeasurements should be(0L)
+ }
+ }
+
+ "record the processing-time of the receive function" in new ActorMetricsFixtures {
+ val trackedActor = createTestActor("measuring-processing-time")
+
+ trackedActor ! TrackTimings(sleep = Some(100 millis))
+ val timings = expectMsgType[TrackedTimings]
+ val snapshot = collectMetricsOf(trackedActor).get
+
+ snapshot.histogram("processing-time").get.numberOfMeasurements should be(1L)
+ snapshot.histogram("processing-time").get.recordsIterator.next().count should be(1L)
+ snapshot.histogram("processing-time").get.recordsIterator.next().level should be(timings.approximateProcessingTime +- 10.millis.toNanos)
+ }
+
+ "record the number of errors" in new ActorMetricsFixtures {
+ val trackedActor = createTestActor("measuring-errors")
+
+ for (i ← 1 to 10) { trackedActor ! Fail }
+ trackedActor ! Ping
+ expectMsg(Pong)
+ val snapshot = collectMetricsOf(trackedActor).get
+
+ snapshot.counter("errors").get.count should be(10)
+ }
+
+ "record the mailbox-size" in new ActorMetricsFixtures {
+ val trackedActor = createTestActor("measuring-mailbox-size")
+
+ trackedActor ! TrackTimings(sleep = Some(100 millis))
+ for (i ← 1 to 10) {
+ trackedActor ! Discard
+ }
+ trackedActor ! Ping
+
+ val timings = expectMsgType[TrackedTimings]
+ expectMsg(Pong)
+ val snapshot = collectMetricsOf(trackedActor).get
+
+ snapshot.minMaxCounter("mailbox-size").get.min should be(0L)
+ snapshot.minMaxCounter("mailbox-size").get.max should be(11L +- 1L)
+ }
+
+ "record the time-in-mailbox" in new ActorMetricsFixtures {
+ val trackedActor = createTestActor("measuring-time-in-mailbox")
+
+ trackedActor ! TrackTimings(sleep = Some(100 millis))
+ val timings = expectMsgType[TrackedTimings]
+ val snapshot = collectMetricsOf(trackedActor).get
+
+ snapshot.histogram("time-in-mailbox").get.numberOfMeasurements should be(1L)
+ snapshot.histogram("time-in-mailbox").get.recordsIterator.next().count should be(1L)
+ snapshot.histogram("time-in-mailbox").get.recordsIterator.next().level should be(timings.approximateTimeInMailbox +- 10.millis.toNanos)
+ }
+
+ "clean up the associated recorder when the actor is stopped" in new ActorMetricsFixtures {
+ val trackedActor = createTestActor("stop")
+ val firstRecorder = actorMetricsRecorderOf(trackedActor).get
+
+ // Killing the actor should remove it's ActorMetrics and registering again bellow should create a new one.
+ val deathWatcher = TestProbe()
+ deathWatcher.watch(trackedActor)
+ trackedActor ! PoisonPill
+ deathWatcher.expectTerminated(trackedActor)
+
+ actorMetricsRecorderOf(trackedActor).get shouldNot be theSameInstanceAs (firstRecorder)
+ }
+ }
+
+ trait ActorMetricsFixtures {
+ val collectionContext = new CollectionContext {
+ val buffer: LongBuffer = LongBuffer.allocate(10000)
+ }
+
+ def actorRecorderName(ref: ActorRef): String = ref.path.elements.mkString("/")
+
+ def actorMetricsRecorderOf(ref: ActorRef): Option[ActorMetrics] =
+ Kamon.metrics.register(ActorMetrics, actorRecorderName(ref)).map(_.recorder)
+
+ def collectMetricsOf(ref: ActorRef): Option[EntitySnapshot] = {
+ Thread.sleep(5) // Just in case the test advances a bit faster than the actor being tested.
+ actorMetricsRecorderOf(ref).map(_.collect(collectionContext))
+ }
+
+ def createTestActor(name: String): ActorRef = {
+ val actor = system.actorOf(Props[ActorMetricsTestActor], name)
+ val initialiseListener = TestProbe()
+
+ // Ensure that the router has been created before returning.
+ actor.tell(Ping, initialiseListener.ref)
+ initialiseListener.expectMsg(Pong)
+
+ // Cleanup all the metric recording instruments:
+ collectMetricsOf(actor)
+
+ actor
+ }
+ }
+}
+
+class ActorMetricsTestActor extends Actor {
+ def receive = {
+ case Discard ⇒
+ case Fail ⇒ throw new ArithmeticException("Division by zero.")
+ case Ping ⇒ sender ! Pong
+ case TrackTimings(sendTimestamp, sleep) ⇒ {
+ val dequeueTimestamp = System.nanoTime()
+ sleep.map(s ⇒ Thread.sleep(s.toMillis))
+ val afterReceiveTimestamp = System.nanoTime()
+
+ sender ! TrackedTimings(sendTimestamp, dequeueTimestamp, afterReceiveTimestamp)
+ }
+ }
+}
+
+object ActorMetricsTestActor {
+ case object Ping
+ case object Pong
+ case object Fail
+ case object Discard
+
+ case class TrackTimings(sendTimestamp: Long = System.nanoTime(), sleep: Option[Duration] = None)
+ case class TrackedTimings(sendTimestamp: Long, dequeueTimestamp: Long, afterReceiveTimestamp: Long) {
+ def approximateTimeInMailbox: Long = dequeueTimestamp - sendTimestamp
+ def approximateProcessingTime: Long = afterReceiveTimestamp - dequeueTimestamp
+ }
+}
diff --git a/kamon-akka/src/test/scala/kamon/akka/DispatcherMetricsSpec.scala b/kamon-akka/src/test/scala/kamon/akka/DispatcherMetricsSpec.scala
new file mode 100644
index 00000000..dd5cfa45
--- /dev/null
+++ b/kamon-akka/src/test/scala/kamon/akka/DispatcherMetricsSpec.scala
@@ -0,0 +1,207 @@
+/* =========================================================================================
+ * Copyright © 2013-2014 the kamon project <http://kamon.io/>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific language governing permissions
+ * and limitations under the License.
+ * =========================================================================================
+ */
+
+package kamon.akka
+
+import akka.actor.ActorRef
+import akka.dispatch.MessageDispatcher
+import akka.testkit.TestProbe
+import com.typesafe.config.ConfigFactory
+import kamon.Kamon
+import kamon.metric.{ EntityRecorder, EntitySnapshot }
+import kamon.testkit.BaseKamonSpec
+
+import scala.concurrent.duration._
+import scala.concurrent.{ Await, Future }
+
+class DispatcherMetricsSpec extends BaseKamonSpec("dispatcher-metrics-spec") {
+ override lazy val config =
+ ConfigFactory.parseString(
+ """
+ |kamon.metric {
+ | tick-interval = 1 hour
+ | default-collection-context-buffer-size = 10
+ |
+ | filters = {
+ | akka-dispatcher {
+ | includes = [ "*" ]
+ | excludes = [ "explicitly-excluded" ]
+ | }
+ | }
+ |
+ | default-instrument-settings {
+ | gauge.refresh-interval = 1 hour
+ | min-max-counter.refresh-interval = 1 hour
+ | }
+ |}
+ |
+ |explicitly-excluded {
+ | type = "Dispatcher"
+ | executor = "fork-join-executor"
+ |}
+ |
+ |tracked-fjp {
+ | type = "Dispatcher"
+ | executor = "fork-join-executor"
+ |
+ | fork-join-executor {
+ | parallelism-min = 8
+ | parallelism-factor = 100.0
+ | parallelism-max = 22
+ | }
+ |}
+ |
+ |tracked-tpe {
+ | type = "Dispatcher"
+ | executor = "thread-pool-executor"
+ |
+ | thread-pool-executor {
+ | core-pool-size-min = 7
+ | core-pool-size-factor = 100.0
+ | max-pool-size-factor = 100.0
+ | max-pool-size-max = 21
+ | }
+ |}
+ |
+ """.stripMargin)
+
+ "the Kamon dispatcher metrics" should {
+ "respect the configured include and exclude filters" in {
+ val defaultDispatcher = forceInit(system.dispatchers.lookup("akka.actor.default-dispatcher"))
+ val fjpDispatcher = forceInit(system.dispatchers.lookup("tracked-fjp"))
+ val tpeDispatcher = forceInit(system.dispatchers.lookup("tracked-tpe"))
+ val excludedDispatcher = forceInit(system.dispatchers.lookup("explicitly-excluded"))
+
+ findDispatcherRecorder(defaultDispatcher) shouldNot be(empty)
+ findDispatcherRecorder(fjpDispatcher) shouldNot be(empty)
+ findDispatcherRecorder(tpeDispatcher) shouldNot be(empty)
+ findDispatcherRecorder(excludedDispatcher) should be(empty)
+ }
+
+ "record metrics for a dispatcher with thread-pool-executor" in {
+ implicit val tpeDispatcher = system.dispatchers.lookup("tracked-tpe")
+ refreshDispatcherInstruments(tpeDispatcher)
+ collectDispatcherMetrics(tpeDispatcher)
+
+ Await.result({
+ Future.sequence {
+ for (_ ← 1 to 100) yield submit(tpeDispatcher)
+ }
+ }, 5 seconds)
+
+ refreshDispatcherInstruments(tpeDispatcher)
+ val snapshot = collectDispatcherMetrics(tpeDispatcher)
+
+ snapshot.gauge("active-threads") should not be empty
+ snapshot.gauge("pool-size").get.min should be >= 7L
+ snapshot.gauge("pool-size").get.max should be <= 21L
+ snapshot.gauge("max-pool-size").get.max should be(21)
+ snapshot.gauge("core-pool-size").get.max should be(21)
+ snapshot.gauge("processed-tasks").get.max should be(102L +- 5L)
+
+ // The processed tasks should be reset to 0 if no more tasks are submitted.
+ val secondSnapshot = collectDispatcherMetrics(tpeDispatcher)
+ secondSnapshot.gauge("processed-tasks").get.max should be(0)
+ }
+
+ "record metrics for a dispatcher with fork-join-executor" in {
+ implicit val fjpDispatcher = system.dispatchers.lookup("tracked-fjp")
+ collectDispatcherMetrics(fjpDispatcher)
+
+ Await.result({
+ Future.sequence {
+ for (_ ← 1 to 100) yield submit(fjpDispatcher)
+ }
+ }, 5 seconds)
+
+ refreshDispatcherInstruments(fjpDispatcher)
+ val snapshot = collectDispatcherMetrics(fjpDispatcher)
+
+ snapshot.minMaxCounter("parallelism").get.max should be(22)
+ snapshot.gauge("pool-size").get.min should be >= 0L
+ snapshot.gauge("pool-size").get.max should be <= 22L
+ snapshot.gauge("active-threads").get.max should be >= 0L
+ snapshot.gauge("running-threads").get.max should be >= 0L
+ snapshot.gauge("queued-task-count").get.max should be(0)
+
+ }
+
+ "clean up the metrics recorders after a dispatcher is shut down" in {
+ implicit val tpeDispatcher = system.dispatchers.lookup("tracked-tpe")
+ implicit val fjpDispatcher = system.dispatchers.lookup("tracked-fjp")
+
+ findDispatcherRecorder(fjpDispatcher) shouldNot be(empty)
+ findDispatcherRecorder(tpeDispatcher) shouldNot be(empty)
+
+ shutdownDispatcher(tpeDispatcher)
+ shutdownDispatcher(fjpDispatcher)
+
+ findDispatcherRecorder(fjpDispatcher) should be(empty)
+ findDispatcherRecorder(tpeDispatcher) should be(empty)
+ }
+
+ }
+
+ def actorRecorderName(ref: ActorRef): String = ref.path.elements.mkString("/")
+
+ def findDispatcherRecorder(dispatcher: MessageDispatcher): Option[EntityRecorder] =
+ Kamon.metrics.find(dispatcher.id, "akka-dispatcher")
+
+ def collectDispatcherMetrics(dispatcher: MessageDispatcher): EntitySnapshot =
+ findDispatcherRecorder(dispatcher).map(_.collect(collectionContext)).get
+
+ def refreshDispatcherInstruments(dispatcher: MessageDispatcher): Unit = {
+ findDispatcherRecorder(dispatcher) match {
+ case Some(tpe: ThreadPoolExecutorDispatcherMetrics) ⇒
+ tpe.processedTasks.refreshValue()
+ tpe.activeThreads.refreshValue()
+ tpe.maxPoolSize.refreshValue()
+ tpe.poolSize.refreshValue()
+ tpe.corePoolSize.refreshValue()
+
+ case Some(fjp: ForkJoinPoolDispatcherMetrics) ⇒
+ fjp.activeThreads.refreshValue()
+ fjp.poolSize.refreshValue()
+ fjp.queuedTaskCount.refreshValue()
+ fjp.paralellism.refreshValues()
+ fjp.runningThreads.refreshValue()
+
+ case other ⇒
+ }
+ }
+
+ def forceInit(dispatcher: MessageDispatcher): MessageDispatcher = {
+ val listener = TestProbe()
+ Future {
+ listener.ref ! "init done"
+ }(dispatcher)
+ listener.expectMsg("init done")
+
+ dispatcher
+ }
+
+ def submit(dispatcher: MessageDispatcher): Future[String] = Future {
+ "hello"
+ }(dispatcher)
+
+ def shutdownDispatcher(dispatcher: MessageDispatcher): Unit = {
+ val shutdownMethod = dispatcher.getClass.getDeclaredMethod("shutdown")
+ shutdownMethod.setAccessible(true)
+ shutdownMethod.invoke(dispatcher)
+ }
+
+ override protected def afterAll(): Unit = system.shutdown()
+}
+
diff --git a/kamon-akka/src/test/scala/kamon/akka/RouterMetricsSpec.scala b/kamon-akka/src/test/scala/kamon/akka/RouterMetricsSpec.scala
new file mode 100644
index 00000000..c4c1d9ad
--- /dev/null
+++ b/kamon-akka/src/test/scala/kamon/akka/RouterMetricsSpec.scala
@@ -0,0 +1,184 @@
+/* =========================================================================================
+ * Copyright © 2013-2014 the kamon project <http://kamon.io/>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific language governing permissions
+ * and limitations under the License.
+ * =========================================================================================
+ */
+
+package kamon.akka
+
+import java.nio.LongBuffer
+
+import akka.actor._
+import akka.routing._
+import akka.testkit.TestProbe
+import com.typesafe.config.ConfigFactory
+import kamon.Kamon
+import kamon.akka.RouterMetricsTestActor._
+import kamon.metric.EntitySnapshot
+import kamon.metric.instrument.CollectionContext
+import kamon.testkit.BaseKamonSpec
+
+import scala.concurrent.duration._
+
+class RouterMetricsSpec extends BaseKamonSpec("router-metrics-spec") {
+ override lazy val config =
+ ConfigFactory.parseString(
+ """
+ |kamon.metric {
+ | tick-interval = 1 hour
+ | default-collection-context-buffer-size = 10
+ |
+ | filters = {
+ | akka-router {
+ | includes = [ "user/tracked-*", "user/measuring-*", "user/stop-*" ]
+ | excludes = [ "user/tracked-explicitly-excluded-*"]
+ | }
+ | }
+ |}
+ |
+ |akka.loglevel = OFF
+ |
+ """.stripMargin)
+
+ "the Kamon router metrics" should {
+ "respect the configured include and exclude filters" in new RouterMetricsFixtures {
+ createTestRouter("tracked-router")
+ createTestRouter("non-tracked-router")
+ createTestRouter("tracked-explicitly-excluded-router")
+
+ routerMetricsRecorderOf("user/tracked-router") should not be empty
+ routerMetricsRecorderOf("user/non-tracked-router") shouldBe empty
+ routerMetricsRecorderOf("user/tracked-explicitly-excluded-router") shouldBe empty
+ }
+
+ "record the routing-time of the receive function for routers" in new RouterMetricsFixtures {
+ val listener = TestProbe()
+ val router = createTestRouter("measuring-routing-time-in-router")
+
+ router.tell(Ping, listener.ref)
+ listener.expectMsg(Pong)
+ val routerSnapshot = collectMetricsOf("user/measuring-routing-time-in-router").get
+
+ routerSnapshot.histogram("routing-time").get.numberOfMeasurements should be(1L)
+ }
+
+ "record the processing-time of the receive function for routers" in new RouterMetricsFixtures {
+ val timingsListener = TestProbe()
+ val router = createTestRouter("measuring-processing-time-in-router")
+
+ router.tell(RouterTrackTimings(sleep = Some(1 second)), timingsListener.ref)
+ val timings = timingsListener.expectMsgType[RouterTrackedTimings]
+ val routerSnapshot = collectMetricsOf("user/measuring-processing-time-in-router").get
+
+ routerSnapshot.histogram("processing-time").get.numberOfMeasurements should be(1L)
+ routerSnapshot.histogram("processing-time").get.recordsIterator.next().count should be(1L)
+ routerSnapshot.histogram("processing-time").get.recordsIterator.next().level should be(timings.approximateProcessingTime +- 10.millis.toNanos)
+ }
+
+ "record the number of errors for routers" in new RouterMetricsFixtures {
+ val listener = TestProbe()
+ val router = createTestRouter("measuring-errors-in-router")
+
+ for (i ← 1 to 10) {
+ router.tell(Fail, listener.ref)
+ }
+
+ router.tell(Ping, listener.ref)
+ listener.expectMsg(Pong)
+
+ val routerSnapshot = collectMetricsOf("user/measuring-errors-in-router").get
+ routerSnapshot.counter("errors").get.count should be(10L)
+ }
+
+ "record the time-in-mailbox for routers" in new RouterMetricsFixtures {
+ val timingsListener = TestProbe()
+ val router = createTestRouter("measuring-time-in-mailbox-in-router")
+
+ router.tell(RouterTrackTimings(sleep = Some(1 second)), timingsListener.ref)
+ val timings = timingsListener.expectMsgType[RouterTrackedTimings]
+ val routerSnapshot = collectMetricsOf("user/measuring-time-in-mailbox-in-router").get
+
+ routerSnapshot.histogram("time-in-mailbox").get.numberOfMeasurements should be(1L)
+ routerSnapshot.histogram("time-in-mailbox").get.recordsIterator.next().count should be(1L)
+ routerSnapshot.histogram("time-in-mailbox").get.recordsIterator.next().level should be(timings.approximateTimeInMailbox +- 10.millis.toNanos)
+ }
+
+ "clean up the associated recorder when the router is stopped" in new RouterMetricsFixtures {
+ val trackedRouter = createTestRouter("stop-in-router")
+ val firstRecorder = routerMetricsRecorderOf("user/stop-in-router").get
+
+ // Killing the router should remove it's RouterMetrics and registering again bellow should create a new one.
+ val deathWatcher = TestProbe()
+ deathWatcher.watch(trackedRouter)
+ trackedRouter ! PoisonPill
+ deathWatcher.expectTerminated(trackedRouter)
+
+ routerMetricsRecorderOf("user/stop-in-router").get shouldNot be theSameInstanceAs (firstRecorder)
+ }
+ }
+
+ trait RouterMetricsFixtures {
+ val collectionContext = new CollectionContext {
+ val buffer: LongBuffer = LongBuffer.allocate(10000)
+ }
+
+ def routerMetricsRecorderOf(routerName: String): Option[RouterMetrics] =
+ Kamon.metrics.register(RouterMetrics, routerName).map(_.recorder)
+
+ def collectMetricsOf(routerName: String): Option[EntitySnapshot] = {
+ Thread.sleep(5) // Just in case the test advances a bit faster than the actor being tested.
+ routerMetricsRecorderOf(routerName).map(_.collect(collectionContext))
+ }
+
+ def createTestRouter(routerName: String): ActorRef = {
+ val router = system.actorOf(Props[RouterMetricsTestActor].withRouter(RoundRobinRouter(nrOfInstances = 5)), routerName)
+ val initialiseListener = TestProbe()
+
+ // Ensure that the router has been created before returning.
+ router.tell(Ping, initialiseListener.ref)
+ initialiseListener.expectMsg(Pong)
+
+ // Cleanup all the metric recording instruments:
+ collectMetricsOf("user/" + routerName)
+
+ router
+ }
+ }
+}
+
+class RouterMetricsTestActor extends Actor {
+ def receive = {
+ case Discard ⇒
+ case Fail ⇒ throw new ArithmeticException("Division by zero.")
+ case Ping ⇒ sender ! Pong
+ case RouterTrackTimings(sendTimestamp, sleep) ⇒ {
+ val dequeueTimestamp = System.nanoTime()
+ sleep.map(s ⇒ Thread.sleep(s.toMillis))
+ val afterReceiveTimestamp = System.nanoTime()
+
+ sender ! RouterTrackedTimings(sendTimestamp, dequeueTimestamp, afterReceiveTimestamp)
+ }
+ }
+}
+
+object RouterMetricsTestActor {
+ case object Ping
+ case object Pong
+ case object Fail
+ case object Discard
+
+ case class RouterTrackTimings(sendTimestamp: Long = System.nanoTime(), sleep: Option[Duration] = None)
+ case class RouterTrackedTimings(sendTimestamp: Long, dequeueTimestamp: Long, afterReceiveTimestamp: Long) {
+ def approximateTimeInMailbox: Long = dequeueTimestamp - sendTimestamp
+ def approximateProcessingTime: Long = afterReceiveTimestamp - dequeueTimestamp
+ }
+}
diff --git a/kamon-akka/src/test/scala/kamon/akka/instrumentation/ActorCellInstrumentationSpec.scala b/kamon-akka/src/test/scala/kamon/akka/instrumentation/ActorCellInstrumentationSpec.scala
new file mode 100644
index 00000000..593a7baa
--- /dev/null
+++ b/kamon-akka/src/test/scala/kamon/akka/instrumentation/ActorCellInstrumentationSpec.scala
@@ -0,0 +1,87 @@
+/* ===================================================
+ * Copyright © 2013 the kamon project <http://kamon.io/>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================================================== */
+package kamon.instrumentation.akka
+
+import akka.actor.{ Actor, Props }
+import akka.pattern.{ ask, pipe }
+import akka.routing._
+import akka.util.Timeout
+import kamon.testkit.BaseKamonSpec
+import kamon.trace.TraceContext
+
+import scala.concurrent.duration._
+
+class ActorCellInstrumentationSpec extends BaseKamonSpec("actor-cell-instrumentation-spec") {
+ implicit lazy val executionContext = system.dispatcher
+
+ "the message passing instrumentation" should {
+ "propagate the TraceContext using bang" in new EchoActorFixture {
+ val testTraceContext = TraceContext.withContext(newContext("bang-reply")) {
+ ctxEchoActor ! "test"
+ TraceContext.currentContext
+ }
+
+ expectMsg(testTraceContext)
+ }
+
+ "propagate the TraceContext using tell" in new EchoActorFixture {
+ val testTraceContext = TraceContext.withContext(newContext("tell-reply")) {
+ ctxEchoActor.tell("test", testActor)
+ TraceContext.currentContext
+ }
+
+ expectMsg(testTraceContext)
+ }
+
+ "propagate the TraceContext using ask" in new EchoActorFixture {
+ implicit val timeout = Timeout(1 seconds)
+ val testTraceContext = TraceContext.withContext(newContext("ask-reply")) {
+ // The pipe pattern use Futures internally, so FutureTracing test should cover the underpinnings of it.
+ (ctxEchoActor ? "test") pipeTo (testActor)
+ TraceContext.currentContext
+ }
+
+ expectMsg(testTraceContext)
+ }
+
+ "propagate the TraceContext to actors behind a pool router" in new RoundRobinRouterFixture {
+ val testTraceContext = TraceContext.withContext(newContext("router-reply")) {
+ router ! "test"
+ TraceContext.currentContext
+ }
+
+ expectMsg(testTraceContext)
+ }
+
+ }
+
+ trait EchoActorFixture {
+ val ctxEchoActor = system.actorOf(Props[TraceContextEcho])
+ }
+
+ trait RoundRobinRouterFixture {
+ val router = system.actorOf(Props[TraceContextEcho].withRouter(
+ RoundRobinRouter(nrOfInstances = 5)), "pool-router")
+ }
+
+}
+
+class TraceContextEcho extends Actor {
+ def receive = {
+ case msg: String ⇒ sender ! TraceContext.currentContext
+ }
+}
+
diff --git a/kamon-akka/src/test/scala/kamon/akka/instrumentation/ActorLoggingInstrumentationSpec.scala b/kamon-akka/src/test/scala/kamon/akka/instrumentation/ActorLoggingInstrumentationSpec.scala
new file mode 100644
index 00000000..143c816d
--- /dev/null
+++ b/kamon-akka/src/test/scala/kamon/akka/instrumentation/ActorLoggingInstrumentationSpec.scala
@@ -0,0 +1,74 @@
+/* ===================================================
+ * Copyright © 2013 the kamon project <http://kamon.io/>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================================================== */
+package kamon.instrumentation.akka
+
+import akka.actor.{ Actor, ActorLogging, Props }
+import akka.event.Logging.LogEvent
+import com.typesafe.config.ConfigFactory
+import kamon.testkit.BaseKamonSpec
+import kamon.trace.TraceLocal.AvailableToMdc
+import kamon.trace.logging.MdcKeysSupport
+import kamon.trace.{ TraceContextAware, TraceLocal, TraceContext }
+import org.scalatest.Inspectors
+import org.slf4j.MDC
+
+class ActorLoggingInstrumentationSpec extends BaseKamonSpec("actor-logging-instrumentation-spec") with Inspectors with MdcKeysSupport {
+ override lazy val config =
+ ConfigFactory.parseString(
+ """
+ |akka {
+ | loggers = ["akka.event.slf4j.Slf4jLogger"]
+ |}
+ """.stripMargin)
+
+ "the ActorLogging instrumentation" should {
+ "attach the TraceContext (if available) to log events" in {
+ val loggerActor = system.actorOf(Props[LoggerActor])
+ system.eventStream.subscribe(testActor, classOf[LogEvent])
+
+ val testTraceContext = TraceContext.withContext(newContext("logging")) {
+ loggerActor ! "info"
+ TraceContext.currentContext
+ }
+
+ fishForMessage() {
+ case event: LogEvent if event.message.toString startsWith "TraceContext" ⇒
+ val ctxInEvent = event.asInstanceOf[TraceContextAware].traceContext
+ ctxInEvent === testTraceContext
+
+ case event: LogEvent ⇒ false
+ }
+ }
+
+ "allow retrieve a value from the MDC when was created a key of type AvailableToMdc" in {
+ val testString = "Hello World"
+ TraceContext.withContext(newContext("logging-with-mdc")) {
+ TraceLocal.store(AvailableToMdc("some-cool-key"))(testString)
+
+ withMdc {
+ MDC.get("other-key") shouldBe (null)
+ MDC.get("some-cool-key") should equal(testString)
+ }
+ }
+ }
+ }
+}
+
+class LoggerActor extends Actor with ActorLogging {
+ def receive = {
+ case "info" ⇒ log.info("TraceContext(name = {}, token = {})", TraceContext.currentContext.name, TraceContext.currentContext.token)
+ }
+}
diff --git a/kamon-akka/src/test/scala/kamon/akka/instrumentation/ActorSystemMessageInstrumentationSpec.scala b/kamon-akka/src/test/scala/kamon/akka/instrumentation/ActorSystemMessageInstrumentationSpec.scala
new file mode 100644
index 00000000..cf5f1b5b
--- /dev/null
+++ b/kamon-akka/src/test/scala/kamon/akka/instrumentation/ActorSystemMessageInstrumentationSpec.scala
@@ -0,0 +1,196 @@
+/*
+ * =========================================================================================
+ * Copyright © 2013-2015 the kamon project <http://kamon.io/>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific language governing permissions
+ * and limitations under the License.
+ * =========================================================================================
+ */
+
+package kamon.instrumentation.akka
+
+import akka.actor.SupervisorStrategy.{ Escalate, Restart, Resume, Stop }
+import akka.actor._
+import akka.testkit.ImplicitSender
+import com.typesafe.config.ConfigFactory
+import kamon.testkit.BaseKamonSpec
+import kamon.trace.{ EmptyTraceContext, TraceContext }
+import org.scalatest.WordSpecLike
+
+import scala.concurrent.duration._
+import scala.util.control.NonFatal
+
+class ActorSystemMessageInstrumentationSpec extends BaseKamonSpec("actor-system-message-instrumentation-spec") with WordSpecLike {
+ override lazy val config =
+ ConfigFactory.parseString(
+ """
+ |akka {
+ | loglevel = OFF
+ |}
+ """.stripMargin)
+
+ implicit lazy val executionContext = system.dispatcher
+
+ "the system message passing instrumentation" should {
+ "keep the TraceContext while processing the Create message in top level actors" in {
+ val testTraceContext = TraceContext.withContext(newContext("creating-top-level-actor")) {
+ system.actorOf(Props(new Actor {
+ testActor ! TraceContext.currentContext
+ def receive: Actor.Receive = { case any ⇒ }
+ }))
+
+ TraceContext.currentContext
+ }
+
+ expectMsg(testTraceContext)
+ }
+
+ "keep the TraceContext while processing the Create message in non top level actors" in {
+ val testTraceContext = TraceContext.withContext(newContext("creating-non-top-level-actor")) {
+ system.actorOf(Props(new Actor {
+ def receive: Actor.Receive = {
+ case any ⇒
+ context.actorOf(Props(new Actor {
+ testActor ! TraceContext.currentContext
+ def receive: Actor.Receive = { case any ⇒ }
+ }))
+ }
+ })) ! "any"
+
+ TraceContext.currentContext
+ }
+
+ expectMsg(testTraceContext)
+ }
+
+ "keep the TraceContext in the supervision cycle" when {
+ "the actor is resumed" in {
+ val supervisor = supervisorWithDirective(Resume)
+
+ val testTraceContext = TraceContext.withContext(newContext("fail-and-resume")) {
+ supervisor ! "fail"
+ TraceContext.currentContext
+ }
+
+ expectMsg(testTraceContext) // From the parent executing the supervision strategy
+
+ // Ensure we didn't tie the actor with the context
+ supervisor ! "context"
+ expectMsg(EmptyTraceContext)
+ }
+
+ "the actor is restarted" in {
+ val supervisor = supervisorWithDirective(Restart, sendPreRestart = true, sendPostRestart = true)
+
+ val testTraceContext = TraceContext.withContext(newContext("fail-and-restart")) {
+ supervisor ! "fail"
+ TraceContext.currentContext
+ }
+
+ expectMsg(testTraceContext) // From the parent executing the supervision strategy
+ expectMsg(testTraceContext) // From the preRestart hook
+ expectMsg(testTraceContext) // From the postRestart hook
+
+ // Ensure we didn't tie the actor with the context
+ supervisor ! "context"
+ expectMsg(EmptyTraceContext)
+ }
+
+ "the actor is stopped" in {
+ val supervisor = supervisorWithDirective(Stop, sendPostStop = true)
+
+ val testTraceContext = TraceContext.withContext(newContext("fail-and-stop")) {
+ supervisor ! "fail"
+ TraceContext.currentContext
+ }
+
+ expectMsg(testTraceContext) // From the parent executing the supervision strategy
+ expectMsg(testTraceContext) // From the postStop hook
+ expectNoMsg(1 second)
+ }
+
+ "the failure is escalated" in {
+ val supervisor = supervisorWithDirective(Escalate, sendPostStop = true)
+
+ val testTraceContext = TraceContext.withContext(newContext("fail-and-escalate")) {
+ supervisor ! "fail"
+ TraceContext.currentContext
+ }
+
+ expectMsg(testTraceContext) // From the parent executing the supervision strategy
+ expectMsg(testTraceContext) // From the grandparent executing the supervision strategy
+ expectMsg(testTraceContext) // From the postStop hook in the child
+ expectMsg(testTraceContext) // From the postStop hook in the parent
+ expectNoMsg(1 second)
+ }
+ }
+ }
+
+ def supervisorWithDirective(directive: SupervisorStrategy.Directive, sendPreRestart: Boolean = false, sendPostRestart: Boolean = false,
+ sendPostStop: Boolean = false, sendPreStart: Boolean = false): ActorRef = {
+ class GrandParent extends Actor {
+ val child = context.actorOf(Props(new Parent))
+
+ override def supervisorStrategy: SupervisorStrategy = OneForOneStrategy() {
+ case NonFatal(throwable) ⇒ testActor ! TraceContext.currentContext; Stop
+ }
+
+ def receive = {
+ case any ⇒ child forward any
+ }
+ }
+
+ class Parent extends Actor {
+ val child = context.actorOf(Props(new Child))
+
+ override def supervisorStrategy: SupervisorStrategy = OneForOneStrategy() {
+ case NonFatal(throwable) ⇒ testActor ! TraceContext.currentContext; directive
+ }
+
+ def receive: Actor.Receive = {
+ case any ⇒ child forward any
+ }
+
+ override def postStop(): Unit = {
+ if (sendPostStop) testActor ! TraceContext.currentContext
+ super.postStop()
+ }
+ }
+
+ class Child extends Actor {
+ def receive = {
+ case "fail" ⇒ throw new ArithmeticException("Division by zero.")
+ case "context" ⇒ sender ! TraceContext.currentContext
+ }
+
+ override def preRestart(reason: Throwable, message: Option[Any]): Unit = {
+ if (sendPreRestart) testActor ! TraceContext.currentContext
+ super.preRestart(reason, message)
+ }
+
+ override def postRestart(reason: Throwable): Unit = {
+ if (sendPostRestart) testActor ! TraceContext.currentContext
+ super.postRestart(reason)
+ }
+
+ override def postStop(): Unit = {
+ if (sendPostStop) testActor ! TraceContext.currentContext
+ super.postStop()
+ }
+
+ override def preStart(): Unit = {
+ if (sendPreStart) testActor ! TraceContext.currentContext
+ super.preStart()
+ }
+ }
+
+ system.actorOf(Props(new GrandParent))
+ }
+}
diff --git a/kamon-akka/src/test/scala/kamon/akka/instrumentation/AskPatternInstrumentationSpec.scala b/kamon-akka/src/test/scala/kamon/akka/instrumentation/AskPatternInstrumentationSpec.scala
new file mode 100644
index 00000000..d925fbf6
--- /dev/null
+++ b/kamon-akka/src/test/scala/kamon/akka/instrumentation/AskPatternInstrumentationSpec.scala
@@ -0,0 +1,137 @@
+/*
+ * =========================================================================================
+ * Copyright © 2013 the kamon project <http://kamon.io/>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific language governing permissions
+ * and limitations under the License.
+ * =========================================================================================
+ */
+
+package kamon.instrumentation.akka
+
+import java.util.concurrent.atomic.AtomicInteger
+
+import akka.actor._
+import akka.event.Logging.Warning
+import akka.pattern.ask
+import akka.testkit.TestProbe
+import akka.util.Timeout
+import com.typesafe.config.ConfigFactory
+import kamon.Kamon
+import kamon.akka.Akka
+import kamon.testkit.BaseKamonSpec
+import kamon.trace.{ TraceContext, TraceContextAware }
+
+import scala.concurrent.duration._
+
+class AskPatternInstrumentationSpec extends BaseKamonSpec("ask-pattern-tracing-spec") {
+ override lazy val config =
+ ConfigFactory.parseString(
+ """
+ |akka {
+ | loglevel = OFF
+ |}
+ """.stripMargin)
+
+ implicit lazy val ec = system.dispatcher
+ implicit val askTimeout = Timeout(10 millis)
+
+ // TODO: Make this work with ActorSelections
+
+ "the AskPatternInstrumentation" when {
+ "configured in heavyweight mode" should {
+ "log a warning with a full stack trace and the TraceContext taken from the moment the ask was triggered for a actor" in new NoReplyFixture {
+ setAskPatternTimeoutWarningMode("heavyweight")
+
+ expectTimeoutWarning() {
+ TraceContext.withContext(newContext("ask-timeout-warning")) {
+ noReplyActorRef ? "hello"
+ TraceContext.currentContext
+ }
+ }
+ }
+ }
+
+ "configured in lightweight mode" should {
+ "log a warning with a short source location description and the TraceContext taken from the moment the ask was triggered for a actor" in new NoReplyFixture {
+ setAskPatternTimeoutWarningMode("lightweight")
+
+ expectTimeoutWarning(messageSizeLimit = Some(1)) {
+ TraceContext.withContext(newContext("ask-timeout-warning")) {
+ noReplyActorRef ? "hello"
+ TraceContext.currentContext
+ }
+ }
+ }
+ }
+
+ "configured in off mode" should {
+ "should not log any warning messages" in new NoReplyFixture {
+ setAskPatternTimeoutWarningMode("off")
+
+ expectTimeoutWarning(expectWarning = false) {
+ TraceContext.withContext(newContext("ask-timeout-warning")) {
+ noReplyActorRef ? "hello"
+ TraceContext.currentContext
+ }
+ }
+ }
+ }
+ }
+
+ def expectTimeoutWarning(messageSizeLimit: Option[Int] = None, expectWarning: Boolean = true)(thunk: ⇒ TraceContext): Unit = {
+ val listener = warningListener()
+ val testTraceContext = thunk
+
+ if (expectWarning) {
+ val warning = listener.fishForMessage() {
+ case Warning(_, _, msg) if msg.toString.startsWith("Timeout triggered for ask pattern registered at") ⇒ true
+ case others ⇒ false
+ }.asInstanceOf[Warning]
+
+ warning.asInstanceOf[TraceContextAware].traceContext should equal(testTraceContext)
+ messageSizeLimit.map { messageLimit ⇒
+ warning.message.toString.lines.size should be(messageLimit)
+ }
+ } else {
+ listener.expectNoMsg()
+ }
+ }
+
+ def warningListener(): TestProbe = {
+ val listener = TestProbe()
+ system.eventStream.subscribe(listener.ref, classOf[Warning])
+ listener
+ }
+
+ def setAskPatternTimeoutWarningMode(mode: String): Unit = {
+ val target = Kamon(Akka)
+ val field = target.getClass.getDeclaredField("askPatternTimeoutWarning")
+ field.setAccessible(true)
+ field.set(target, mode)
+ }
+
+ val fixtureCounter = new AtomicInteger(0)
+
+ trait NoReplyFixture {
+ def noReplyActorRef: ActorRef = system.actorOf(Props[NoReply], "no-reply-" + fixtureCounter.incrementAndGet())
+
+ def noReplyActorSelection: ActorSelection = {
+ val target = noReplyActorRef
+ system.actorSelection(target.path)
+ }
+ }
+}
+
+class NoReply extends Actor {
+ def receive = {
+ case any ⇒
+ }
+}