├── project
├── build.properties
└── plugins.sbt
├── version.sbt
├── .travis.yml
├── test
├── common
│ └── src
│ │ └── test
│ │ ├── scala
│ │ └── kamon
│ │ │ └── instrumentation
│ │ │ └── akka
│ │ │ ├── ContextTesting.scala
│ │ │ ├── AkkaTestKitInstrumentation.scala
│ │ │ ├── remote
│ │ │ └── MessageBufferTest.scala
│ │ │ ├── ContextEchoActor.scala
│ │ │ ├── RouterMetricsTestActor.scala
│ │ │ ├── ActorMetricsTestActor.scala
│ │ │ ├── ActorLoggingInstrumentationSpec.scala
│ │ │ ├── EnvelopeSpec.scala
│ │ │ ├── sharding
│ │ │ ├── ShardingMessageBufferingSpec.scala
│ │ │ └── ShardingInstrumentationSpec.scala
│ │ │ ├── AskPatternInstrumentationSpec.scala
│ │ │ ├── ActorSystemMetricsSpec.scala
│ │ │ ├── AutoGroupingSpec.scala
│ │ │ ├── ActorCellInstrumentationSpec.scala
│ │ │ ├── DispatcherMetricsSpec.scala
│ │ │ └── ActorMetricsSpec.scala
│ │ └── resources
│ │ ├── logback.xml
│ │ └── application.conf
├── akka-2.6
│ └── src
│ │ └── test
│ │ └── scala
│ │ └── kamon
│ │ └── instrumentation
│ │ └── akka
│ │ └── remote
│ │ └── ArteryTcpRemotingInstrumentationSpec.scala
├── akka-2.5
│ └── src
│ │ └── test
│ │ └── scala
│ │ └── kamon
│ │ └── instrumentation
│ │ └── akka
│ │ └── remote
│ │ ├── ArteryTcpRemotingInstrumentationSpec.scala
│ │ └── RemotingInstrumentationSpec.scala
└── akka-2.4
│ └── src
│ └── test
│ └── scala
│ └── kamon
│ └── instrumentation
│ └── akka
│ └── remote
│ └── RemotingInstrumentationSpec.scala
├── instrumentation
├── common
│ └── src
│ │ └── main
│ │ ├── scala
│ │ └── kamon
│ │ │ └── instrumentation
│ │ │ └── akka
│ │ │ ├── remote
│ │ │ ├── internal
│ │ │ │ └── KamonOptionVal.scala
│ │ │ ├── MessageBufferInstrumentation.scala
│ │ │ └── ShardingInstrumentation.scala
│ │ │ ├── AkkaRemoteInstrumentation.scala
│ │ │ ├── instrumentations
│ │ │ ├── VersionFiltering.scala
│ │ │ ├── SystemMessageInstrumentation.scala
│ │ │ ├── ActorRefInstrumentation.scala
│ │ │ ├── EnvelopeInstrumentation.scala
│ │ │ ├── ActorLoggingInstrumentation.scala
│ │ │ ├── DispatcherInfo.scala
│ │ │ ├── EventStreamInstrumentation.scala
│ │ │ ├── internal
│ │ │ │ └── ReplaceWithMethodInterceptor.scala
│ │ │ ├── RouterInstrumentation.scala
│ │ │ ├── RouterMonitor.scala
│ │ │ ├── ActorCellInfo.scala
│ │ │ ├── AskPatternInstrumentation.scala
│ │ │ └── ActorInstrumentation.scala
│ │ │ ├── AkkaRemoteMetrics.scala
│ │ │ ├── AkkaClusterShardingMetrics.scala
│ │ │ ├── AkkaInstrumentation.scala
│ │ │ └── AkkaMetrics.scala
│ │ └── java
│ │ └── kamon
│ │ └── instrumentation
│ │ └── akka
│ │ └── instrumentations
│ │ ├── ActorCellInvokeAdvice.java
│ │ └── AkkaPrivateAccess.java
├── akka-2.5
│ └── src
│ │ └── main
│ │ ├── scala-2.11
│ │ └── kamon
│ │ │ └── instrumentation
│ │ │ └── akka
│ │ │ └── instrumentations
│ │ │ └── akka_26
│ │ │ ├── remote
│ │ │ └── RemotingInstrumentation.scala
│ │ │ └── DispatcherInstrumentation.scala
│ │ ├── protobuf
│ │ ├── ContextAwareWireFormats.proto
│ │ └── WireFormats.proto
│ │ └── scala
│ │ └── kamon
│ │ └── instrumentation
│ │ └── akka
│ │ └── instrumentations
│ │ └── akka_25
│ │ └── remote
│ │ ├── internal
│ │ ├── AkkaPduProtobufCodecDecodeMessageMethodAdvisor.scala
│ │ ├── AkkaPduProtobufCodecConstructMessageMethodInterceptor.scala
│ │ └── ArterySerializationAdvice.scala
│ │ └── RemotingInstrumentation.scala
└── akka-2.6
│ └── src
│ └── main
│ ├── protobuf
│ ├── ContextAwareWireFormats.proto
│ └── WireFormats.proto
│ └── scala
│ └── kamon
│ └── instrumentation
│ └── akka
│ └── instrumentations
│ └── akka_26
│ ├── remote
│ ├── internal
│ │ ├── AkkaPduProtobufCodecDecodeMessageMethodAdvisor.scala
│ │ ├── AkkaPduProtobufCodecConstructMessageMethodInterceptor.scala
│ │ └── ArterySerializationAdvice.scala
│ └── RemotingInstrumentation.scala
│ └── DispatcherInstrumentation.scala
├── README.md
├── LICENSE
├── .gitignore
├── travis-test.sh
├── bench
└── src
│ └── main
│ └── scala
│ └── kamon
│ └── instrumentation
│ └── akka
│ └── ActorCreationBench.scala
└── CONTRIBUTING.md
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=1.2.8
--------------------------------------------------------------------------------
/version.sbt:
--------------------------------------------------------------------------------
1 | version in ThisBuild := "2.0.4-SNAPSHOT"
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: scala
2 | script:
3 | - ./travis-test.sh
4 | scala:
5 | - 2.11.8
6 | jdk:
7 | - openjdk8
8 | before_script:
9 | - mkdir $TRAVIS_BUILD_DIR/tmp
10 | - export SBT_OPTS="-Djava.io.tmpdir=$TRAVIS_BUILD_DIR/tmp"
11 | sudo: false
12 |
13 |
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | lazy val root: Project = project.in(file(".")).dependsOn(latestSbtUmbrella)
2 | lazy val latestSbtUmbrella = ProjectRef(uri("git://github.com/kamon-io/kamon-sbt-umbrella.git#kamon-2.x"), "kamon-sbt-umbrella")
3 |
4 | addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.7")
5 |
--------------------------------------------------------------------------------
/test/common/src/test/scala/kamon/instrumentation/akka/ContextTesting.scala:
--------------------------------------------------------------------------------
1 | package kamon.instrumentation.akka
2 |
3 | import kamon.context.Context
4 | import kamon.tag.TagSet
5 |
6 | object ContextTesting {
7 | val TestKey = "testkey"
8 | def testContext(value: String) = Context.of(TagSet.of(TestKey, value))
9 | }
10 |
--------------------------------------------------------------------------------
/instrumentation/common/src/main/scala/kamon/instrumentation/akka/remote/internal/KamonOptionVal.scala:
--------------------------------------------------------------------------------
1 | package akka
2 |
3 | import akka.util.{ OptionVal => AkkaOptionVal }
4 | /**
5 | * The sole purpose of this object is to provide access to the otherwise internal class [[akka.util.OptionVal]].
6 | */
7 | object KamonOptionVal {
8 | type OptionVal[+T >: Null] = AkkaOptionVal[T]
9 | }
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # This reporsitory has been moved.
2 |
3 | Since March 2020 all the Kamon instrumentation and reporting modules were moved to Kamon's main repository at [https://github.com/kamon-io/kamon](https://github.com/kamon-io/kamon). Please check out the main repository for the latest sources, reporting issues or start contributing. You can also stop by our [Gitter Channel](https://gitter.im/kamon-io/Kamon).
--------------------------------------------------------------------------------
/instrumentation/akka-2.5/src/main/scala-2.11/kamon/instrumentation/akka/instrumentations/akka_26/remote/RemotingInstrumentation.scala:
--------------------------------------------------------------------------------
1 | package kamon.instrumentation.akka.instrumentations.akka_26.remote
2 |
3 | import kanela.agent.api.instrumentation.InstrumentationBuilder
4 |
5 | // Only exists to avoid warnings of this class not existing when running on Scala 2.11
6 | class RemotingInstrumentation extends InstrumentationBuilder {}
--------------------------------------------------------------------------------
/test/common/src/test/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | This software is licensed under the Apache 2 license, quoted below.
2 |
3 | Copyright © 2013-2014 the kamon project
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License"); you may not
6 | use this file except in compliance with the License. You may obtain a copy of
7 | the License at
8 |
9 | [http://www.apache.org/licenses/LICENSE-2.0]
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 | License for the specific language governing permissions and limitations under
15 | the License.
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.class
2 | *.log
3 | .history
4 | *.sc
5 | .pygments-cache
6 | .DS_Store
7 |
8 | # sbt specific
9 | dist/*
10 | target/
11 | lib_managed/
12 | src_managed/
13 | project/boot/
14 | project/plugins/project/
15 | .ensime
16 | .ensime_cache
17 |
18 | # Scala-IDE specific
19 | .scala_dependencies
20 | .idea
21 | .idea_modules
22 |
23 | # Intellij
24 | .idea/
25 | *.iml
26 | *.iws
27 |
28 | # Eclipse
29 | .project
30 | .settings
31 | .classpath
32 | .cache
33 | .cache-main
34 | .cache-tests
35 | bin/
36 |
37 | _site
38 |
39 | # Ignore Play! working directory #
40 | db
41 | eclipse
42 | lib
43 | log
44 | logs
45 | modules
46 | precompiled
47 | project/project
48 | project/target
49 | target
50 | tmp
51 | test-result
52 | server.pid
53 | *.iml
54 | *.eml
55 |
56 | # Default sigar library provision location.
57 | native/
58 |
--------------------------------------------------------------------------------
/instrumentation/akka-2.5/src/main/protobuf/ContextAwareWireFormats.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto2";
2 | import "WireFormats.proto";
3 |
4 |
5 | option java_package = "akka.remote";
6 | option optimize_for = SPEED;
7 |
8 |
9 | /************************************************
10 | * Kamon-specific additions to the protocol
11 | ************************************************/
12 |
13 | message AckAndContextAwareEnvelopeContainer {
14 | optional AcknowledgementInfo ack = 1;
15 | optional ContextAwareRemoteEnvelope envelope = 2;
16 | }
17 |
18 | message ContextAwareRemoteEnvelope {
19 | required ActorRefData recipient = 1;
20 | required SerializedMessage message = 2;
21 | optional ActorRefData sender = 4;
22 | optional fixed64 seq = 5;
23 |
24 | optional RemoteContext traceContext = 15;
25 | }
26 |
27 | message RemoteContext {
28 | required bytes context = 1;
29 | }
30 |
31 |
--------------------------------------------------------------------------------
/instrumentation/akka-2.6/src/main/protobuf/ContextAwareWireFormats.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto2";
2 | import "WireFormats.proto";
3 |
4 |
5 | option java_package = "akka.remote";
6 | option optimize_for = SPEED;
7 |
8 |
9 | /************************************************
10 | * Kamon-specific additions to the protocol
11 | ************************************************/
12 |
13 | message AckAndContextAwareEnvelopeContainer {
14 | optional AcknowledgementInfo ack = 1;
15 | optional ContextAwareRemoteEnvelope envelope = 2;
16 | }
17 |
18 | message ContextAwareRemoteEnvelope {
19 | required ActorRefData recipient = 1;
20 | required SerializedMessage message = 2;
21 | optional ActorRefData sender = 4;
22 | optional fixed64 seq = 5;
23 |
24 | optional RemoteContext traceContext = 15;
25 | }
26 |
27 | message RemoteContext {
28 | required bytes context = 1;
29 | }
30 |
31 |
--------------------------------------------------------------------------------
/instrumentation/common/src/main/scala/kamon/instrumentation/akka/AkkaRemoteInstrumentation.scala:
--------------------------------------------------------------------------------
1 | package kamon.instrumentation.akka
2 |
3 | import java.time.Duration
4 |
5 | import com.typesafe.config.Config
6 | import kamon.Kamon
7 |
8 |
9 | object AkkaRemoteInstrumentation {
10 |
11 | @volatile private var _settings = readSettings(Kamon.config())
12 | Kamon.onReconfigure(newConfig => _settings = readSettings(newConfig))
13 |
14 | def settings(): Settings =
15 | _settings
16 |
17 | private def readSettings(config: Config): Settings =
18 | Settings(
19 | config.getBoolean("kamon.instrumentation.akka.remote.track-serialization-metrics"),
20 | config.getDuration("kamon.instrumentation.akka.cluster-sharding.shard-metrics-sample-interval")
21 | )
22 |
23 | case class Settings(
24 | trackSerializationMetrics: Boolean,
25 | shardMetricsSampleInterval: Duration
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/instrumentation/common/src/main/scala/kamon/instrumentation/akka/remote/MessageBufferInstrumentation.scala:
--------------------------------------------------------------------------------
1 | package kamon.instrumentation.akka.remote
2 |
3 |
4 | import _root_.kanela.agent.api.instrumentation.InstrumentationBuilder
5 | import kamon.instrumentation.context.{CaptureCurrentContextOnExit, HasContext, InvokeWithCapturedContext}
6 |
7 | class MessageBufferInstrumentation extends InstrumentationBuilder {
8 |
9 | /**
10 | * Ensures that the Context traveling with outgoing messages will be properly propagated if those messages are
11 | * temporarily held on a MessageBuffer. This happens, for example, when sending messages to shard that has not yet
12 | * started.
13 | */
14 | onType("akka.util.MessageBuffer$Node")
15 | .mixin(classOf[HasContext.Mixin])
16 | .advise(isConstructor, CaptureCurrentContextOnExit)
17 | .advise(method("apply"), InvokeWithCapturedContext)
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/travis-test.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # Licensed under the Apache License, Version 2.0
3 | # Adapted from https://github.com/paulp/psp-std/blob/master/bin/test
4 |
5 | runTests () {
6 | sbt +test || exit 1
7 |
8 | echo "[info] $(date) - finished sbt test"
9 | }
10 |
11 | stripTerminalEscapeCodes () {
12 | sed -r "s/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[mGKM]//g"
13 | }
14 |
15 | mkRegex () { ( IFS="|" && echo "$*" ); }
16 |
17 | filterOutput() {
18 | while read line; do
19 | if ! [[ $(echo $line | stripTerminalEscapeCodes) =~ $excludeRegex ]] ; then
20 | echo $line
21 | fi
22 | done
23 | }
24 |
25 | main() {
26 | # sbt output filter
27 | local excludeRegex=$(mkRegex \
28 | '\[info\] (Resolving|Loading|Updating|Packaging|Done updating|downloading| \[SUCCESSFUL \])' \
29 | 're[-]run with [-]unchecked for details' \
30 | 'one warning found'
31 | )
32 |
33 | echo "[info] $(date) - starting sbt test"
34 | (set -o pipefail && runTests |& filterOutput)
35 | }
36 |
37 | main $@
38 |
--------------------------------------------------------------------------------
/test/common/src/test/scala/kamon/instrumentation/akka/AkkaTestKitInstrumentation.scala:
--------------------------------------------------------------------------------
1 | package kamon.instrumentation.akka
2 |
3 | import kanela.agent.api.instrumentation.InstrumentationBuilder
4 | import kanela.agent.libs.net.bytebuddy.asm.Advice
5 |
6 | class AkkaTestKitInstrumentation extends InstrumentationBuilder {
7 |
8 | /**
9 | * We believe that tests fail randomly because every now and then the tests thread receives a message from one of the
10 | * echo actors and continues processing before the execution of the receive function on the echo actor's thread
11 | * finishes and metrics are recorded. This instrumentation delays the waiting on the test thread to get better
12 | * chances that the echo actor receive finishes.
13 | */
14 | onSubTypesOf("akka.testkit.TestKitBase")
15 | .advise(method("receiveOne"), DelayReceiveOne)
16 | }
17 |
18 | object DelayReceiveOne {
19 |
20 | @Advice.OnMethodExit(suppress = classOf[Throwable])
21 | def exit(): Unit =
22 | Thread.sleep(5)
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/instrumentation/common/src/main/scala/kamon/instrumentation/akka/instrumentations/VersionFiltering.scala:
--------------------------------------------------------------------------------
1 | package kamon.instrumentation.akka.instrumentations
2 |
3 | /**
4 | * Helps applying certain instrumentations only if Akka or a specific version of Akka is present.
5 | */
6 | trait VersionFiltering {
7 |
8 | /**
9 | * Runs the code block if Akka is known to be present.
10 | */
11 | def onAkka(block: => Unit): Unit = {
12 | if(akkaVersion().nonEmpty)
13 | block
14 | }
15 |
16 | /**
17 | * Runs the code block if a version of Akka starting with the provided version is known to be present.
18 | */
19 | def onAkka(version: String*)(block: => Unit): Unit = {
20 | if(akkaVersion().filter(av => version.exists(v => av.startsWith(v))).isDefined)
21 | block
22 | }
23 |
24 | // This should only succeed when Akka is on the classpath.
25 | private def akkaVersion(): Option[String] = {
26 | try {
27 | Option(akka.Version.current)
28 | } catch {
29 | case _: Throwable => None
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/test/common/src/test/scala/kamon/instrumentation/akka/remote/MessageBufferTest.scala:
--------------------------------------------------------------------------------
1 | package kamon.instrumentation.akka.remote
2 |
3 | import akka.actor.Actor
4 | import akka.util.MessageBuffer
5 | import kamon.Kamon
6 | import kamon.context.Context
7 | import org.scalatest.{Matchers, WordSpec}
8 |
9 | class MessageBufferTest extends WordSpec with Matchers {
10 |
11 | "the MessageBuffer instrumentation" should {
12 | "remember the current context when appending message and apply it when foreach is called when used directly" in {
13 | val messageBuffer = MessageBuffer.empty
14 | val key = Context.key("some_key", "")
15 |
16 | Kamon.runWithContext(Context.of(key, "some_value")) {
17 | messageBuffer.append("scala", Actor.noSender)
18 | }
19 |
20 | Kamon.currentContext().get(key) shouldBe ""
21 |
22 | var iterated = false
23 | messageBuffer.foreach { (msg, ref) =>
24 | iterated = true
25 | Kamon.currentContext().get(key) shouldBe "some_value"
26 | }
27 |
28 | iterated shouldBe true
29 |
30 | }
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/instrumentation/akka-2.5/src/main/scala-2.11/kamon/instrumentation/akka/instrumentations/akka_26/DispatcherInstrumentation.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * =========================================================================================
3 | * Copyright © 2013-2018 the kamon project
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
6 | * except in compliance with the License. You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software distributed under the
11 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
12 | * either express or implied. See the License for the specific language governing permissions
13 | * and limitations under the License.
14 | * =========================================================================================
15 | */
16 |
17 | package kamon.instrumentation.akka.instrumentations.akka_26
18 |
19 | import kanela.agent.api.instrumentation.InstrumentationBuilder
20 |
21 | // Only exists to avoid warnings of this class not existing when running on Scala 2.11
22 | class DispatcherInstrumentation extends InstrumentationBuilder {}
--------------------------------------------------------------------------------
/test/common/src/test/scala/kamon/instrumentation/akka/ContextEchoActor.scala:
--------------------------------------------------------------------------------
1 | package kamon.instrumentation.akka
2 |
3 | import akka.actor._
4 | import akka.remote.RemoteScope
5 | import kamon.Kamon
6 | import kamon.tag.Lookups._
7 |
8 | class ContextEchoActor(creationListener: Option[ActorRef]) extends Actor with ActorLogging {
9 |
10 | creationListener foreach { recipient =>
11 | recipient ! currentTraceContextInfo
12 | }
13 |
14 | def receive = {
15 | case "die" =>
16 | throw new ArithmeticException("Division by zero.")
17 |
18 | case "reply-trace-token" =>
19 | sender ! currentTraceContextInfo
20 | }
21 |
22 | def currentTraceContextInfo: String = {
23 | val ctx = Kamon.currentContext()
24 | val name = ctx.getTag(option(ContextEchoActor.EchoTag)).getOrElse("")
25 | s"name=$name"
26 | }
27 | }
28 |
29 | object ContextEchoActor {
30 |
31 | val EchoTag = "tests"
32 |
33 | def props(creationListener: Option[ActorRef]): Props =
34 | Props(classOf[ContextEchoActor], creationListener)
35 |
36 | def remoteProps(creationTraceContextListener: Option[ActorRef], remoteAddress: Address): Props =
37 | Props(classOf[ContextEchoActor], creationTraceContextListener)
38 | .withDeploy(Deploy(scope = RemoteScope(remoteAddress)))
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/instrumentation/common/src/main/scala/kamon/instrumentation/akka/instrumentations/SystemMessageInstrumentation.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * =========================================================================================
3 | * Copyright © 2013-2018 the kamon project
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
6 | * except in compliance with the License. You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software distributed under the
11 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
12 | * either express or implied. See the License for the specific language governing permissions
13 | * and limitations under the License.
14 | * =========================================================================================
15 | */
16 |
17 | package kamon.instrumentation.akka.instrumentations
18 |
19 | import kamon.instrumentation.context.HasContext
20 | import kanela.agent.api.instrumentation.InstrumentationBuilder
21 |
22 | class SystemMessageInstrumentation extends InstrumentationBuilder {
23 |
24 | /**
25 | * Captures the current Context when a System Message is created.
26 | */
27 | onSubTypesOf("akka.dispatch.sysmsg.SystemMessage")
28 | .mixin(classOf[HasContext.MixinWithInitializer])
29 | }
--------------------------------------------------------------------------------
/instrumentation/akka-2.5/src/main/scala/kamon/instrumentation/akka/instrumentations/akka_25/remote/internal/AkkaPduProtobufCodecDecodeMessageMethodAdvisor.scala:
--------------------------------------------------------------------------------
1 | package akka.kamon.instrumentation.akka.instrumentations.akka_25.remote
2 |
3 | import akka.actor.Address
4 | import akka.remote.RemoteActorRefProvider
5 | import akka.util.ByteString
6 | import kamon.Kamon
7 | import kamon.context.BinaryPropagation.ByteStreamReader
8 | import kamon.instrumentation.akka.AkkaRemoteMetrics
9 | import kanela.agent.libs.net.bytebuddy.asm.Advice.{Argument, OnMethodEnter}
10 | import akka.remote.ContextAwareWireFormats.{AckAndContextAwareEnvelopeContainer}
11 |
12 | /**
13 | * Advisor for akka.remote.transport.AkkaPduProtobufCodec$::decodeMessage
14 | */
15 | class AkkaPduProtobufCodecDecodeMessage
16 |
17 | object AkkaPduProtobufCodecDecodeMessage {
18 |
19 | @OnMethodEnter
20 | def enter(@Argument(0) bs: ByteString, @Argument(1) provider: RemoteActorRefProvider, @Argument(2) localAddress: Address): Unit = {
21 | val ackAndEnvelope = AckAndContextAwareEnvelopeContainer.parseFrom(bs.toArray)
22 | if (ackAndEnvelope.hasEnvelope && ackAndEnvelope.getEnvelope.hasTraceContext) {
23 | val remoteCtx = ackAndEnvelope.getEnvelope.getTraceContext
24 |
25 | if(remoteCtx.getContext.size() > 0) {
26 | val ctx = Kamon.defaultBinaryPropagation().read(ByteStreamReader.of(remoteCtx.getContext.toByteArray))
27 | Kamon.storeContext(ctx)
28 | }
29 |
30 | val messageSize = ackAndEnvelope.getEnvelope.getMessage.getMessage.size()
31 | AkkaRemoteMetrics.serializationInstruments(localAddress.system).inboundMessageSize.record(messageSize)
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/instrumentation/akka-2.6/src/main/scala/kamon/instrumentation/akka/instrumentations/akka_26/remote/internal/AkkaPduProtobufCodecDecodeMessageMethodAdvisor.scala:
--------------------------------------------------------------------------------
1 | package akka.kamon.instrumentation.akka.instrumentations.akka_26.remote.internal
2 |
3 | import akka.actor.Address
4 | import akka.remote.ContextAwareWireFormats_Akka26.AckAndContextAwareEnvelopeContainer
5 | import akka.remote.RemoteActorRefProvider
6 | import akka.util.ByteString
7 | import kamon.Kamon
8 | import kamon.context.BinaryPropagation.ByteStreamReader
9 | import kamon.instrumentation.akka.AkkaRemoteMetrics
10 | import kanela.agent.libs.net.bytebuddy.asm.Advice.{Argument, OnMethodEnter}
11 |
12 | /**
13 | * Advisor for akka.remote.transport.AkkaPduProtobufCodec$::decodeMessage
14 | */
15 | class AkkaPduProtobufCodecDecodeMessage
16 |
17 | object AkkaPduProtobufCodecDecodeMessage {
18 |
19 | @OnMethodEnter
20 | def enter(@Argument(0) bs: ByteString, @Argument(1) provider: RemoteActorRefProvider, @Argument(2) localAddress: Address): Unit = {
21 | val ackAndEnvelope = AckAndContextAwareEnvelopeContainer.parseFrom(bs.toArray)
22 | if (ackAndEnvelope.hasEnvelope && ackAndEnvelope.getEnvelope.hasTraceContext) {
23 | val remoteCtx = ackAndEnvelope.getEnvelope.getTraceContext
24 |
25 | if(remoteCtx.getContext.size() > 0) {
26 | val ctx = Kamon.defaultBinaryPropagation().read(ByteStreamReader.of(remoteCtx.getContext.toByteArray))
27 | Kamon.storeContext(ctx)
28 | }
29 |
30 | val messageSize = ackAndEnvelope.getEnvelope.getMessage.getMessage.size()
31 | AkkaRemoteMetrics.serializationInstruments(localAddress.system).inboundMessageSize.record(messageSize)
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/bench/src/main/scala/kamon/instrumentation/akka/ActorCreationBench.scala:
--------------------------------------------------------------------------------
1 | package kamon.instrumentation.akka
2 |
3 | import java.time
4 | import java.time.Instant
5 | import java.util.concurrent.TimeUnit
6 | import java.util.concurrent.atomic.AtomicLong
7 |
8 | import akka.actor.{Actor, ActorRef, ActorSystem, PoisonPill, Props}
9 | import org.openjdk.jmh.annotations._
10 |
11 | import scala.concurrent.duration._
12 | import scala.concurrent.Await
13 |
14 | @State(Scope.Benchmark)
15 | @BenchmarkMode(Array(Mode.AverageTime))
16 | @Fork(2)
17 | @Warmup(iterations = 4, time = 5)
18 | @Measurement(iterations = 10, time = 5)
19 | class ActorCreationBench {
20 | val system = ActorSystem("ActorCreationBench")
21 | val activeActorsCount = new AtomicLong()
22 | val props = Props(new DummyCreationActor(activeActorsCount))
23 | val baseName = "creation-test-actor"
24 | var lastIndex = 0
25 |
26 | @Benchmark
27 | @OutputTimeUnit(TimeUnit.MICROSECONDS)
28 | def createActor(): ActorRef = {
29 | lastIndex += 1
30 | system.actorOf(props, baseName + lastIndex)
31 | }
32 |
33 | @TearDown(Level.Iteration)
34 | def cleanupActors(): Unit = {
35 | system.actorSelection("/user/*").tell(PoisonPill, Actor.noSender)
36 | while(activeActorsCount.get() > 0) { Thread.sleep(50) }
37 | }
38 |
39 | @TearDown
40 | def stopActorSystem(): Unit = {
41 | system.terminate()
42 | Await.ready(system.whenTerminated, 30 seconds)
43 | }
44 | }
45 |
46 | class DummyCreationActor(activeCounter: AtomicLong) extends Actor {
47 | activeCounter.incrementAndGet()
48 |
49 | override def receive: Receive = {
50 | case _ =>
51 | }
52 |
53 | override def postStop(): Unit = {
54 | super.postStop()
55 | activeCounter.decrementAndGet()
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/instrumentation/common/src/main/scala/kamon/instrumentation/akka/AkkaRemoteMetrics.scala:
--------------------------------------------------------------------------------
1 | package kamon.instrumentation.akka
2 |
3 | import kamon.Kamon
4 | import kamon.metric.InstrumentGroup
5 | import kamon.metric.MeasurementUnit.information
6 | import kamon.tag.TagSet
7 |
8 | import scala.collection.concurrent.TrieMap
9 |
10 | object AkkaRemoteMetrics {
11 |
12 | val InboundMessageSize = Kamon.histogram (
13 | name = "akka.remote.messages.inbound.size",
14 | description = "Tracks the distribution of inbound message sizes",
15 | unit = information.bytes
16 | )
17 |
18 | val OutboundMessageSize = Kamon.histogram (
19 | name = "akka.remote.messages.outbound.size",
20 | description = "Tracks the distribution of outbound message sizes",
21 | unit = information.bytes
22 | )
23 |
24 | val SerializationTime = Kamon.timer (
25 | name = "akka.remote.serialization-time",
26 | description = "Tracks the time taken to serialize outgoing messages"
27 | )
28 |
29 | val DeserializationTime = Kamon.timer (
30 | name = "akka.remote.deserialization-time",
31 | description = "Tracks the time taken to deserialize incoming messages"
32 | )
33 |
34 | private val _serializationInstrumentsCache = TrieMap.empty[String, SerializationInstruments]
35 |
36 | class SerializationInstruments(systemName: String) extends InstrumentGroup(TagSet.of("system", systemName)) {
37 | val inboundMessageSize = register(InboundMessageSize)
38 | val outboundMessageSize = register(OutboundMessageSize)
39 | val serializationTime = register(SerializationTime)
40 | val deserializationTime = register(DeserializationTime)
41 | }
42 |
43 | def serializationInstruments(system: String): SerializationInstruments =
44 | _serializationInstrumentsCache.atomicGetOrElseUpdate(system, new SerializationInstruments(system))
45 | }
46 |
--------------------------------------------------------------------------------
/instrumentation/common/src/main/scala/kamon/instrumentation/akka/instrumentations/ActorRefInstrumentation.scala:
--------------------------------------------------------------------------------
1 | package kamon.instrumentation.akka.instrumentations
2 |
3 | import kamon.Kamon
4 | import kamon.context.Storage.Scope
5 | import kamon.instrumentation.context.HasContext
6 | import kanela.agent.api.instrumentation.InstrumentationBuilder
7 | import kanela.agent.libs.net.bytebuddy.asm.Advice
8 |
9 | class ActorRefInstrumentation extends InstrumentationBuilder {
10 |
11 | /**
12 | * This instrumentation helps with keeping a track of types in the entire actor path of any given actor, which allows
13 | * to have proper information when evaluating auto-grouping.
14 | */
15 | onTypes("akka.actor.LocalActorRef", "akka.actor.RepointableActorRef")
16 | .mixin(classOf[HasGroupPath.Mixin])
17 |
18 | /**
19 | * This ensures that if there was any Context available when an Actor was created, it will also be available when its
20 | * messages are being transferred from the Unstarted cell to the actual cell.
21 | */
22 | onType("akka.actor.RepointableActorRef")
23 | .mixin(classOf[HasContext.MixinWithInitializer])
24 | .advise(method("point"), RepointableActorRefPointAdvice)
25 | }
26 |
27 | trait HasGroupPath {
28 | def groupPath: String
29 | def setGroupPath(groupPath: String): Unit
30 | }
31 |
32 | object HasGroupPath {
33 |
34 | class Mixin(@volatile var groupPath: String) extends HasGroupPath {
35 | override def setGroupPath(groupPath: String): Unit =
36 | this.groupPath = groupPath
37 | }
38 | }
39 |
40 | object RepointableActorRefPointAdvice {
41 |
42 | @Advice.OnMethodEnter
43 | def enter(@Advice.This repointableActorRef: Object): Scope =
44 | Kamon.storeContext(repointableActorRef.asInstanceOf[HasContext].context)
45 |
46 | @Advice.OnMethodExit
47 | def exit(@Advice.Enter scope: Scope): Unit =
48 | scope.close()
49 | }
50 |
51 |
52 |
--------------------------------------------------------------------------------
/instrumentation/common/src/main/java/kamon/instrumentation/akka/instrumentations/ActorCellInvokeAdvice.java:
--------------------------------------------------------------------------------
1 | package kamon.instrumentation.akka.instrumentations;
2 |
3 | import akka.dispatch.Envelope;
4 | import kamon.context.Context;
5 | import kamon.instrumentation.context.HasContext;
6 | import kamon.instrumentation.context.HasTimestamp;
7 | import kanela.agent.libs.net.bytebuddy.asm.Advice;
8 |
9 | final public class ActorCellInvokeAdvice {
10 |
11 | @Advice.OnMethodEnter(suppress = Throwable.class)
12 | public static void enter(
13 | @Advice.This Object cell,
14 | @Advice.Argument(0) Object envelope,
15 | @Advice.Local("stateFromStart") Object stateFromStart,
16 | @Advice.Local("processingStartTimestamp") Long processingStartTimestamp,
17 | @Advice.Local("envelopeTimestamp") Long envelopeTimestamp,
18 | @Advice.Local("context") Context context) {
19 |
20 | final ActorMonitor actorMonitor = ((HasActorMonitor) cell).actorMonitor();
21 |
22 | processingStartTimestamp = actorMonitor.captureProcessingStartTimestamp();
23 | context = ((HasContext) envelope).context();
24 | envelopeTimestamp = ((HasTimestamp) envelope).timestamp();
25 | stateFromStart = actorMonitor.onMessageProcessingStart(context, envelopeTimestamp, (Envelope) envelope);
26 | }
27 |
28 | @Advice.OnMethodExit(suppress = Throwable.class)
29 | public static void exit(
30 | @Advice.This Object cell,
31 | @Advice.Local("stateFromStart") Object stateFromStart,
32 | @Advice.Local("processingStartTimestamp") Long processingStartTimestamp,
33 | @Advice.Local("envelopeTimestamp") Long envelopeTimestamp,
34 | @Advice.Local("context") Context context) {
35 |
36 | final ActorMonitor actorMonitor = ((HasActorMonitor) cell).actorMonitor();
37 | actorMonitor.onMessageProcessingEnd(context, envelopeTimestamp, processingStartTimestamp, stateFromStart);
38 | }
39 |
40 | }
--------------------------------------------------------------------------------
/instrumentation/common/src/main/scala/kamon/instrumentation/akka/instrumentations/EnvelopeInstrumentation.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * =========================================================================================
3 | * Copyright © 2013-2018 the kamon project
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
6 | * except in compliance with the License. You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software distributed under the
11 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
12 | * either express or implied. See the License for the specific language governing permissions
13 | * and limitations under the License.
14 | * =========================================================================================
15 | */
16 |
17 | package kamon.instrumentation.akka.instrumentations
18 |
19 | import kamon.instrumentation.context.{HasContext, HasTimestamp}
20 | import kanela.agent.api.instrumentation.InstrumentationBuilder
21 | import kanela.agent.libs.net.bytebuddy.asm.Advice
22 |
23 |
24 | class EnvelopeInstrumentation extends InstrumentationBuilder {
25 |
26 | /**
27 | * Ensures that the Akka Envelope is able to carry around a Context and a Timestamp.
28 | */
29 | onType("akka.dispatch.Envelope")
30 | .mixin(classOf[HasContext.Mixin])
31 | .mixin(classOf[HasTimestamp.Mixin])
32 | .advise(method("copy"), EnvelopeCopyAdvice)
33 | }
34 |
35 | object EnvelopeCopyAdvice {
36 |
37 | @Advice.OnMethodExit
38 | def exit(@Advice.Return newEnvelope: Any, @Advice.This envelope: Any): Unit = {
39 | newEnvelope.asInstanceOf[HasContext].setContext(envelope.asInstanceOf[HasContext].context)
40 | newEnvelope.asInstanceOf[HasTimestamp].setTimestamp(envelope.asInstanceOf[HasTimestamp].timestamp)
41 | }
42 | }
43 |
44 |
--------------------------------------------------------------------------------
/instrumentation/common/src/main/scala/kamon/instrumentation/akka/instrumentations/ActorLoggingInstrumentation.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * =========================================================================================
3 | * Copyright © 2013-2018 the kamon project
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
6 | * except in compliance with the License. You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software distributed under the
11 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
12 | * either express or implied. See the License for the specific language governing permissions
13 | * and limitations under the License.
14 | * =========================================================================================
15 | */
16 |
17 | package kamon.instrumentation.akka.instrumentations
18 |
19 | import akka.event.Logging.LogEvent
20 | import kamon.Kamon
21 | import kamon.context.Storage.Scope
22 | import kamon.instrumentation.context.HasContext
23 | import kanela.agent.api.instrumentation.InstrumentationBuilder
24 | import kanela.agent.libs.net.bytebuddy.asm.Advice.{Argument, Enter, OnMethodEnter, OnMethodExit}
25 |
26 | class ActorLoggingInstrumentation extends InstrumentationBuilder {
27 |
28 | /**
29 | * Captures the Context that was present when a logging event was created and then sets it as current when it is
30 | * being processed by the logging actor.
31 | */
32 | onSubTypesOf("akka.event.Logging$LogEvent")
33 | .mixin(classOf[HasContext.MixinWithInitializer])
34 |
35 | onType("akka.event.slf4j.Slf4jLogger")
36 | .advise(method("withMdc"), WithMdcMethodAdvice)
37 | }
38 |
39 | object WithMdcMethodAdvice {
40 |
41 | @OnMethodEnter
42 | def enter(@Argument(1) logEvent: LogEvent): Scope =
43 | Kamon.storeContext(logEvent.asInstanceOf[HasContext].context)
44 |
45 | @OnMethodExit
46 | def exit(@Enter scope: Scope): Unit =
47 | scope.close()
48 | }
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | Contributing to Kamon
2 | =====================
3 |
4 | Thanks for your intention on collaborating to the Kamon Project! It doesn't matter if you want to provide a small change
5 | to our docs, are lost in configuration or want contribute a brand new feature, we value all of your contributions and
6 | the time you take to use our tool and prepare a contribution, we only ask you to follow this guidance depending on your
7 | situation:
8 |
9 | If you are experiencing a bug
10 | -----------------------------
11 |
12 | If you see weird exceptions in your log or something definitely is working improperly please [open an issue] and include
13 | the Kamon, Akka and Spray/Play! versions that you are using along with as many useful information you can find related
14 | to the issue. If you can provide a gist or a short way to reproduce the issue we will be more than happy!
15 |
16 | If you don't know what is wrong
17 | -------------------------------
18 |
19 | If you don't see any metrics at all or features are not working maybe you have a setup or configuration problem, to
20 | address this kind of problems please send us a emails to our [mailing list] and we will reply as soon as we can! Again,
21 | please include the relevant version and current setup information to speed up the process. If you are in doubt of
22 | whether you have a bug or a configuration problem, email us and we will take care of openning a issue if necessary.
23 |
24 | If you want to make a code contribution to the project
25 | ------------------------------------------------------
26 |
27 | Awesome! First, please note that we try to follow the [commit message conventions] used by the Spray guys and we need
28 | you to electronically fill our [CLA] before accepting your contribution. Also, if your PR contains various commits,
29 | please squash them into a single commit. Let the PR rain begin!
30 |
31 |
32 | [open an issue]: https://github.com/kamon-io/Kamon/issues/new
33 | [mailing list]: https://groups.google.com/forum/#!forum/kamon-user
34 | [commit message conventions]: http://spray.io/project-info/contributing/
35 | [CLA]: https://docs.google.com/forms/d/1G_IDrBTFzOMwHvhxfKRBwNtpRelSa_MZ6jecH8lpTlc/viewform
36 |
--------------------------------------------------------------------------------
/instrumentation/common/src/main/scala/kamon/instrumentation/akka/instrumentations/DispatcherInfo.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * =========================================================================================
3 | * Copyright © 2013-2018 the kamon project
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
6 | * except in compliance with the License. You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software distributed under the
11 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
12 | * either express or implied. See the License for the specific language governing permissions
13 | * and limitations under the License.
14 | * =========================================================================================
15 | */
16 |
17 | package kamon.instrumentation.akka.instrumentations
18 |
19 | import akka.dispatch.DispatcherPrerequisites
20 |
21 | object DispatcherInfo {
22 |
23 | trait HasDispatcherPrerequisites {
24 | def dispatcherPrerequisites: DispatcherPrerequisites
25 | def setDispatcherPrerequisites(dispatcherPrerequisites: DispatcherPrerequisites): Unit
26 | }
27 |
28 | object HasDispatcherPrerequisites {
29 | class Mixin extends HasDispatcherPrerequisites {
30 | @volatile private var _dispatcherPrerequisites: DispatcherPrerequisites = _
31 | override def dispatcherPrerequisites: DispatcherPrerequisites = _dispatcherPrerequisites
32 | override def setDispatcherPrerequisites(dispatcherPrerequisites: DispatcherPrerequisites): Unit =
33 | _dispatcherPrerequisites = dispatcherPrerequisites
34 | }
35 | }
36 |
37 | trait HasDispatcherName {
38 | def dispatcherName: String
39 | def setDispatcherName(dispatcherName: String): Unit
40 | }
41 |
42 | object HasDispatcherName {
43 | class Mixin extends HasDispatcherName {
44 | @volatile private var _dispatcherName: String = _
45 | override def dispatcherName: String = _dispatcherName
46 | override def setDispatcherName(dispatcherName: String): Unit = _dispatcherName = dispatcherName
47 | }
48 | }
49 | }
--------------------------------------------------------------------------------
/test/common/src/test/scala/kamon/instrumentation/akka/RouterMetricsTestActor.scala:
--------------------------------------------------------------------------------
1 | /* =========================================================================================
2 | * Copyright © 2013-2017 the kamon project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5 | * except in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the
10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
11 | * either express or implied. See the License for the specific language governing permissions
12 | * and limitations under the License.
13 | * =========================================================================================
14 | */
15 |
16 | package kamon.instrumentation.akka
17 |
18 | import akka.actor._
19 | import kamon.Kamon
20 |
21 | import scala.concurrent.duration._
22 |
23 | class RouterMetricsTestActor extends Actor {
24 | import RouterMetricsTestActor._
25 | override def receive = {
26 | case Discard =>
27 | case Die => context.stop(self)
28 | case Fail => throw new ArithmeticException("Division by zero.")
29 | case Ping => sender ! Pong
30 | case RouterTrackTimings(sendTimestamp, sleep) => {
31 | val dequeueTimestamp = Kamon.clock().nanos()
32 | sleep.map(s => Thread.sleep(s.toMillis))
33 | val afterReceiveTimestamp = Kamon.clock().nanos()
34 |
35 | sender ! RouterTrackedTimings(sendTimestamp, dequeueTimestamp, afterReceiveTimestamp)
36 | }
37 | }
38 | }
39 |
40 | object RouterMetricsTestActor {
41 | case object Ping
42 | case object Pong
43 | case object Fail
44 | case object Discard
45 | case object Die
46 |
47 | case class RouterTrackTimings(sendTimestamp: Long = Kamon.clock().nanos(), sleep: Option[Duration] = None)
48 | case class RouterTrackedTimings(sendTimestamp: Long, dequeueTimestamp: Long, afterReceiveTimestamp: Long) {
49 | def approximateTimeInMailbox: Long = dequeueTimestamp - sendTimestamp
50 | def approximateProcessingTime: Long = afterReceiveTimestamp - dequeueTimestamp
51 | }
52 | }
53 |
54 |
--------------------------------------------------------------------------------
/instrumentation/common/src/main/java/kamon/instrumentation/akka/instrumentations/AkkaPrivateAccess.java:
--------------------------------------------------------------------------------
1 | package kamon.instrumentation.akka.instrumentations;
2 |
3 | import akka.actor.*;
4 | import akka.dispatch.Mailbox;
5 | import akka.dispatch.sysmsg.SystemMessage;
6 | import akka.pattern.PromiseActorRef;
7 | import akka.routing.RoutedActorCell;
8 | import akka.routing.RoutedActorRef;
9 | import scala.Option;
10 |
11 | /**
12 | * This class exposes access to several private[akka] members that wouldn't be visible from the Scala codebase.
13 | */
14 | public class AkkaPrivateAccess {
15 |
16 | public static boolean isSystemMessage(Object message) {
17 | return message instanceof SystemMessage;
18 | }
19 |
20 | public static boolean isPromiseActorRef(ActorRef ref) {
21 | return ref instanceof PromiseActorRef;
22 | }
23 |
24 | public static boolean isInternalAndActiveActorRef(ActorRef target) {
25 | return target != null && target instanceof InternalActorRef && !((InternalActorRef) target).isTerminated();
26 | }
27 |
28 | public static boolean isRoutedActorRef(ActorRef target) {
29 | return target instanceof RoutedActorRef;
30 | }
31 |
32 | public static boolean isRoutedActorCell(Object cell) {
33 | return cell instanceof RoutedActorCell;
34 | }
35 |
36 | public static boolean isUnstartedActorCell(Object cell) {
37 | return cell instanceof UnstartedCell;
38 | }
39 |
40 | public static Class> unstartedActorCellClass() {
41 | return UnstartedCell.class;
42 | }
43 |
44 | public static boolean isDeadLettersMailbox(Object cell, Object mailbox) {
45 | final ActorCell actorCell = (ActorCell) cell;
46 | return mailbox == actorCell.dispatcher().mailboxes().deadLetterMailbox();
47 | }
48 |
49 | public static long mailboxMessageCount(Object mailbox) {
50 | return ((Mailbox) mailbox).numberOfMessages();
51 | }
52 |
53 | public static Option cellProps(Object cell) {
54 | if(cell != null && cell instanceof Cell)
55 | return Option.apply(((Cell) cell).props());
56 | else
57 | return Option.empty();
58 | }
59 |
60 | public static Option lookupDeploy(ActorPath path, ActorSystem system) {
61 | final Deployer deployer = new Deployer(system.settings(), ((ExtendedActorSystem) system).dynamicAccess());
62 | return deployer.lookup(path.$div("$a"));
63 | }
64 | }
--------------------------------------------------------------------------------
/test/common/src/test/scala/kamon/instrumentation/akka/ActorMetricsTestActor.scala:
--------------------------------------------------------------------------------
1 | /* =========================================================================================
2 | * Copyright © 2013-2017 the kamon project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5 | * except in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the
10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
11 | * either express or implied. See the License for the specific language governing permissions
12 | * and limitations under the License.
13 | * =========================================================================================
14 | */
15 |
16 | package kamon.instrumentation.akka
17 |
18 | import akka.actor._
19 | import kamon.Kamon
20 |
21 | import scala.concurrent.duration._
22 |
23 | class ActorMetricsTestActor extends Actor {
24 | import ActorMetricsTestActor._
25 |
26 | override def receive = {
27 | case Discard =>
28 | case Die => context.stop(self)
29 | case Fail => throw new ArithmeticException("Division by zero.")
30 | case Ping => sender ! Pong
31 | case Block(forDuration) =>
32 | Thread.sleep(forDuration.toMillis)
33 | case BlockAndDie(forDuration) =>
34 | Thread.sleep(forDuration.toMillis)
35 | context.stop(self)
36 | case TrackTimings(sendTimestamp, sleep) => {
37 | val dequeueTimestamp = Kamon.clock().nanos()
38 | sleep.map(s => Thread.sleep(s.toMillis))
39 | val afterReceiveTimestamp = Kamon.clock().nanos()
40 |
41 | sender ! TrackedTimings(sendTimestamp, dequeueTimestamp, afterReceiveTimestamp)
42 | }
43 | }
44 | }
45 |
46 | object ActorMetricsTestActor {
47 | case object Ping
48 | case object Pong
49 | case object Fail
50 | case object Die
51 | case object Discard
52 |
53 | case class Block(duration: Duration)
54 | case class BlockAndDie(duration: Duration)
55 | case class TrackTimings(sendTimestamp: Long = Kamon.clock().nanos(), sleep: Option[Duration] = None)
56 | case class TrackedTimings(sendTimestamp: Long, dequeueTimestamp: Long, afterReceiveTimestamp: Long) {
57 | def approximateTimeInMailbox: Long = dequeueTimestamp - sendTimestamp
58 | def approximateProcessingTime: Long = afterReceiveTimestamp - dequeueTimestamp
59 | }
60 | }
61 |
62 |
--------------------------------------------------------------------------------
/test/common/src/test/scala/kamon/instrumentation/akka/ActorLoggingInstrumentationSpec.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * =========================================================================================
3 | * Copyright © 2013-2017 the kamon project
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
6 | * except in compliance with the License. You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software distributed under the
11 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
12 | * either express or implied. See the License for the specific language governing permissions
13 | * and limitations under the License.
14 | * =========================================================================================
15 | */
16 | package kamon.instrumentation.akka
17 |
18 |
19 | import akka.actor.{Actor, ActorLogging, ActorSystem, Props}
20 | import akka.event.Logging.LogEvent
21 | import akka.testkit.{ImplicitSender, TestKit}
22 | import kamon.Kamon
23 | import kamon.instrumentation.context.HasContext
24 | import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike}
25 | import kamon.tag.Lookups._
26 |
27 | class ActorLoggingInstrumentationSpec extends TestKit(ActorSystem("ActorCellInstrumentationSpec")) with WordSpecLike
28 | with BeforeAndAfterAll with Matchers with ImplicitSender {
29 | import ContextTesting._
30 |
31 | "the ActorLogging instrumentation" should {
32 | "capture the current context and attach it to log events" in {
33 | val loggerActor = system.actorOf(Props[LoggerActor])
34 | Kamon.runWithContext(testContext("propagate-when-logging")) {
35 | loggerActor ! "info"
36 | }
37 |
38 | val logEvent = fishForMessage() {
39 | case event: LogEvent if event.message.toString startsWith "TestLogEvent" => true
40 | case _: LogEvent => false
41 | }
42 |
43 | Kamon.runWithContext(logEvent.asInstanceOf[HasContext].context) {
44 | val keyValueFromContext = Kamon.currentContext().getTag(option(ContextTesting.TestKey)).getOrElse("Missing Context Tag")
45 | keyValueFromContext should be("propagate-when-logging")
46 | }
47 | }
48 | }
49 |
50 |
51 | override protected def beforeAll(): Unit = system.eventStream.subscribe(testActor, classOf[LogEvent])
52 |
53 | override protected def afterAll(): Unit = shutdown()
54 | }
55 |
56 | class LoggerActor extends Actor with ActorLogging {
57 | def receive = {
58 | case "info" => log.info("TestLogEvent")
59 | }
60 | }
61 |
62 |
--------------------------------------------------------------------------------
/instrumentation/common/src/main/scala/kamon/instrumentation/akka/instrumentations/EventStreamInstrumentation.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * =========================================================================================
3 | * Copyright © 2013-2018 the kamon project
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
6 | * except in compliance with the License. You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software distributed under the
11 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
12 | * either express or implied. See the License for the specific language governing permissions
13 | * and limitations under the License.
14 | * =========================================================================================
15 | */
16 |
17 | package kamon.instrumentation.akka.instrumentations
18 |
19 | import akka.actor.{ActorSystem, DeadLetter, UnhandledMessage}
20 | import kamon.instrumentation.akka.AkkaMetrics
21 | import kanela.agent.api.instrumentation.InstrumentationBuilder
22 | import kanela.agent.libs.net.bytebuddy.asm.Advice.{Argument, OnMethodExit, This}
23 |
24 | class EventStreamInstrumentation extends InstrumentationBuilder {
25 |
26 | /**
27 | * Counts dead letters and unhandled messages as they are published on the EventStream.
28 | */
29 | onType("akka.event.EventStream")
30 | .mixin(classOf[HasSystem.Mixin])
31 | .advise(isConstructor.and(takesArguments(2)), ConstructorAdvice)
32 | .advise(method("publish").and(takesArguments(1)), PublishMethodAdvice)
33 | }
34 |
35 |
36 | object ConstructorAdvice {
37 |
38 | @OnMethodExit(suppress = classOf[Throwable])
39 | def exit(@This eventStream: HasSystem, @Argument(0) system:ActorSystem): Unit = {
40 | eventStream.setSystem(system)
41 | }
42 | }
43 |
44 | object PublishMethodAdvice {
45 |
46 | @OnMethodExit(suppress = classOf[Throwable])
47 | def exit(@This stream:HasSystem, @Argument(0) event: AnyRef):Unit = event match {
48 | case _: DeadLetter => AkkaMetrics.forSystem(stream.system.name).deadLetters.increment()
49 | case _: UnhandledMessage => AkkaMetrics.forSystem(stream.system.name).unhandledMessages.increment()
50 | case _ => ()
51 | }
52 | }
53 |
54 | trait HasSystem {
55 | def system: ActorSystem
56 | def setSystem(system: ActorSystem): Unit
57 | }
58 |
59 | object HasSystem {
60 |
61 | class Mixin(var system: ActorSystem) extends HasSystem {
62 |
63 | override def setSystem(system: ActorSystem): Unit =
64 | this.system = system
65 | }
66 | }
--------------------------------------------------------------------------------
/test/common/src/test/scala/kamon/instrumentation/akka/EnvelopeSpec.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * =========================================================================================
3 | * Copyright © 2017 the kamon project
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
6 | * except in compliance with the License. You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software distributed under the
11 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
12 | * either express or implied. See the License for the specific language governing permissions
13 | * and limitations under the License.
14 | * =========================================================================================
15 | */
16 |
17 | package kamon.instrumentation.akka
18 |
19 |
20 | import akka.actor.{ActorSystem, ExtendedActorSystem, Props}
21 | import akka.dispatch.Envelope
22 | import akka.testkit.{ImplicitSender, TestKit}
23 | import kamon.Kamon
24 | import kamon.instrumentation.context.{HasContext, HasTimestamp}
25 | import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike}
26 |
27 | class EnvelopeSpec extends TestKit(ActorSystem("EnvelopeSpec")) with WordSpecLike with Matchers
28 | with BeforeAndAfterAll with ImplicitSender {
29 |
30 | "EnvelopeInstrumentation" should {
31 | "mixin EnvelopeContext" in {
32 | val actorRef = system.actorOf(Props[NoReply])
33 | val env = Envelope("msg", actorRef, system).asInstanceOf[Object]
34 | env match {
35 | case e: Envelope with HasContext with HasTimestamp =>
36 | e.setContext(Kamon.currentContext())
37 | e.setTimestamp(Kamon.clock().nanos())
38 |
39 | case _ => fail("InstrumentedEnvelope is not mixed in")
40 | }
41 | env match {
42 | case s: Serializable => {
43 | import java.io._
44 | val bos = new ByteArrayOutputStream
45 | val oos = new ObjectOutputStream(bos)
46 | oos.writeObject(env)
47 | oos.close()
48 | akka.serialization.JavaSerializer.currentSystem.withValue(system.asInstanceOf[ExtendedActorSystem]) {
49 | val ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()))
50 | val obj = ois.readObject()
51 | ois.close()
52 | obj match {
53 | case e: Envelope with HasContext with HasTimestamp =>
54 | e.timestamp should not be 0L
55 | e.context should not be null
56 | case _ => fail("InstrumentedEnvelope is not mixed in")
57 | }
58 | }
59 | }
60 | case _ => fail("envelope is not serializable")
61 | }
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/test/common/src/test/scala/kamon/instrumentation/akka/sharding/ShardingMessageBufferingSpec.scala:
--------------------------------------------------------------------------------
1 | package kamon.instrumentation.akka.sharding
2 |
3 | import akka.actor._
4 | import akka.cluster.Cluster
5 | import akka.cluster.sharding.{ClusterSharding, ClusterShardingSettings, ShardRegion}
6 | import akka.testkit.{ImplicitSender, TestKitBase}
7 | import com.typesafe.config.ConfigFactory
8 | import kamon.Kamon
9 | import kamon.context.Context
10 | import kamon.instrumentation.akka.ContextEchoActor
11 | import kamon.testkit.MetricInspection
12 | import org.scalatest.{Matchers, WordSpecLike}
13 | import scala.concurrent.duration._
14 |
15 | class ShardingMessageBufferingSpec extends TestKitBase with WordSpecLike with Matchers with ImplicitSender with MetricInspection.Syntax {
16 |
17 | implicit lazy val system: ActorSystem = {
18 | ActorSystem("cluster-sharding-spec-system", ConfigFactory.parseString(
19 | """
20 | |akka {
21 | | loglevel = INFO
22 | | loggers = [ "akka.event.slf4j.Slf4jLogger" ]
23 | |
24 | | actor {
25 | | provider = "cluster"
26 | | }
27 | | remote {
28 | | enabled-transports = ["akka.remote.netty.tcp"]
29 | | netty.tcp {
30 | | hostname = "127.0.0.1"
31 | | port = 2556
32 | | }
33 | | }
34 | |}
35 | """.stripMargin))
36 | }
37 |
38 | val remoteSystem: ActorSystem = ActorSystem("cluster-sharding-spec-remote-system", ConfigFactory.parseString(
39 | """
40 | |akka {
41 | | loglevel = INFO
42 | | loggers = [ "akka.event.slf4j.Slf4jLogger" ]
43 | |
44 | | actor {
45 | | provider = "cluster"
46 | | }
47 | | remote {
48 | | enabled-transports = ["akka.remote.netty.tcp"]
49 | | netty.tcp {
50 | | hostname = "127.0.0.1"
51 | | port = 2557
52 | | }
53 | | }
54 | |}
55 | """.stripMargin))
56 |
57 | def contextWithBroadcast(name: String): Context =
58 | Context.Empty.withTag(
59 | ContextEchoActor.EchoTag, name
60 | )
61 |
62 | val extractEntityId: ShardRegion.ExtractEntityId = {
63 | case entityId:String => (entityId, "reply-trace-token")
64 | }
65 | val extractShardId: ShardRegion.ExtractShardId = {
66 | case entityId:String => (entityId.toInt % 10).toString
67 | }
68 |
69 | "The MessageBuffer instrumentation" should {
70 | "propagate the current Context when sending message to a sharding region that has not been started" in {
71 | Cluster(system).join(Cluster(system).selfAddress)
72 | Cluster(remoteSystem).join(Cluster(system).selfAddress)
73 |
74 | val replierRegion: ActorRef = ClusterSharding(system).start(
75 | typeName = "replier",
76 | entityProps = ContextEchoActor.props(None),
77 | settings = ClusterShardingSettings(system),
78 | extractEntityId = extractEntityId,
79 | extractShardId = extractShardId)
80 |
81 | Kamon.runWithContext(contextWithBroadcast("cluster-sharding-actor-123")) {
82 | replierRegion ! "123"
83 | }
84 |
85 | expectMsg(10 seconds, "name=cluster-sharding-actor-123")
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/instrumentation/common/src/main/scala/kamon/instrumentation/akka/instrumentations/internal/ReplaceWithMethodInterceptor.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * =========================================================================================
3 | * Copyright © 2013-2018 the kamon project
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
6 | * except in compliance with the License. You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software distributed under the
11 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
12 | * either express or implied. See the License for the specific language governing permissions
13 | * and limitations under the License.
14 | * =========================================================================================
15 | */
16 |
17 | package akka.instrumentation
18 |
19 | import java.util.concurrent.locks.ReentrantLock
20 |
21 | import akka.actor.{Cell, UnstartedCell}
22 | import akka.dispatch.Envelope
23 | import akka.dispatch.sysmsg.{LatestFirstSystemMessageList, SystemMessage, SystemMessageList}
24 | import kamon.Kamon
25 | import kamon.instrumentation.context.HasContext
26 | import kanela.agent.libs.net.bytebuddy.implementation.bind.annotation.{Argument, RuntimeType, This}
27 |
28 | object ReplaceWithMethodInterceptor {
29 |
30 | @RuntimeType
31 | def aroundReplaceWithInRepointableActorRef(@This unStartedCell: Object, @Argument(0) cell: Object): Unit = {
32 | import kamon.instrumentation.akka.instrumentations.ActorInstrumentation._
33 |
34 | val queue = unstartedCellQueueField.get(unStartedCell).asInstanceOf[java.util.LinkedList[_]]
35 | val lock = unstartedCellLockField.get(unStartedCell).asInstanceOf[ReentrantLock]
36 | val sysQueueHead = systemMsgQueueField.get(unStartedCell).asInstanceOf[SystemMessage]
37 |
38 | def locked[T](body: => T): T = {
39 | lock.lock()
40 | try body finally lock.unlock()
41 | }
42 |
43 | var sysQueue = new LatestFirstSystemMessageList(sysQueueHead)
44 |
45 | locked {
46 | try {
47 | def drainSysmsgQueue(): Unit = {
48 | // using while in case a sys msg enqueues another sys msg
49 | while (sysQueue.nonEmpty) {
50 | var sysQ = sysQueue.reverse
51 | sysQueue = SystemMessageList.LNil
52 | while (sysQ.nonEmpty) {
53 | val msg = sysQ.head
54 | sysQ = sysQ.tail
55 | msg.unlink()
56 | cell.asInstanceOf[Cell].sendSystemMessage(msg)
57 | }
58 | }
59 | }
60 |
61 | drainSysmsgQueue()
62 |
63 | while (!queue.isEmpty) {
64 | queue.poll() match {
65 | case e: HasContext =>
66 | Kamon.runWithContext(e.context) {
67 | cell.asInstanceOf[Cell].sendMessage(e.asInstanceOf[Envelope])
68 | }
69 | }
70 |
71 | drainSysmsgQueue()
72 | }
73 | } finally {
74 | unStartedCell.asInstanceOf[UnstartedCell].self.swapCell(cell.asInstanceOf[Cell])
75 | }
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/instrumentation/common/src/main/scala/kamon/instrumentation/akka/instrumentations/RouterInstrumentation.scala:
--------------------------------------------------------------------------------
1 | package kamon.instrumentation.akka.instrumentations
2 |
3 | import akka.actor.{ActorRef, ActorSystem, Props}
4 | import kanela.agent.api.instrumentation.InstrumentationBuilder
5 | import kanela.agent.libs.net.bytebuddy.asm.Advice._
6 |
7 | class RouterInstrumentation extends InstrumentationBuilder {
8 |
9 | /**
10 | * Provides the router metrics tracking implementation.
11 | */
12 | onType("akka.routing.RoutedActorCell")
13 | .mixin(classOf[HasRouterMonitor.Mixin])
14 | .advise(isConstructor, RoutedActorCellConstructorAdvice)
15 | .advise(method("sendMessage").and(takesArguments(1)), SendMessageAdvice)
16 | .advise(method("sendMessage").and(takesArguments(1)), SendMessageOnRouterAdvice)
17 |
18 | /**
19 | * Captures the router and routee Props so that we can properly apply tags to the router metrics.
20 | */
21 | onType("akka.routing.RoutedActorRef")
22 | .mixin(classOf[HasRouterProps.Mixin])
23 | .advise(isConstructor, RoutedActorRefConstructorAdvice)
24 | }
25 |
26 |
27 | /**
28 | * Helps with capturing the Props for both the router and the routees.
29 | */
30 | trait HasRouterProps {
31 | def routeeProps: Props
32 | def routerProps: Props
33 | def setRouteeProps(props: Props): Unit
34 | def setRouterProps(props: Props): Unit
35 | }
36 |
37 | object HasRouterProps {
38 |
39 | class Mixin(var routeeProps: Props, var routerProps: Props) extends HasRouterProps {
40 |
41 | override def setRouteeProps(props: Props): Unit =
42 | this.routeeProps = props
43 |
44 | override def setRouterProps(props: Props): Unit =
45 | this.routerProps = props
46 | }
47 | }
48 |
49 | trait HasRouterMonitor {
50 | def routerMonitor: RouterMonitor
51 | def setRouterMonitor(routerMonitor: RouterMonitor): Unit
52 | }
53 |
54 | object HasRouterMonitor {
55 |
56 | class Mixin(var routerMonitor: RouterMonitor) extends HasRouterMonitor {
57 |
58 | override def setRouterMonitor(routerMonitor: RouterMonitor): Unit =
59 | this.routerMonitor = routerMonitor
60 | }
61 | }
62 |
63 | object RoutedActorRefConstructorAdvice {
64 |
65 | @OnMethodExit(suppress = classOf[Throwable])
66 | def exit(@This ref: ActorRef, @Argument(1) routerProps: Props, @Argument(4) routeeProps: Props): Unit = {
67 | val routedRef = ref.asInstanceOf[HasRouterProps]
68 | routedRef.setRouteeProps(routeeProps)
69 | routedRef.setRouterProps(routerProps)
70 | }
71 | }
72 |
73 | object RoutedActorCellConstructorAdvice {
74 |
75 | @OnMethodExit(suppress = classOf[Throwable])
76 | def exit(@This cell: Any, @Argument(0) system: ActorSystem, @Argument(1) ref: ActorRef, @Argument(5) parent: ActorRef): Unit = {
77 | cell.asInstanceOf[HasRouterMonitor].setRouterMonitor(RouterMonitor.from(cell, ref, parent, system))
78 | }
79 | }
80 |
81 | object SendMessageOnRouterAdvice {
82 |
83 | def routerInstrumentation(cell: Any): RouterMonitor =
84 | cell.asInstanceOf[HasRouterMonitor].routerMonitor
85 |
86 | @OnMethodEnter(suppress = classOf[Throwable])
87 | def onEnter(@This cell: Any): Long =
88 | routerInstrumentation(cell).processMessageStart()
89 |
90 | @OnMethodExit(suppress = classOf[Throwable])
91 | def onExit(@This cell: Any, @Enter timestampBeforeProcessing: Long): Unit =
92 | routerInstrumentation(cell).processMessageEnd(timestampBeforeProcessing)
93 | }
94 |
95 |
--------------------------------------------------------------------------------
/instrumentation/common/src/main/scala/kamon/instrumentation/akka/instrumentations/RouterMonitor.scala:
--------------------------------------------------------------------------------
1 | package kamon.instrumentation.akka.instrumentations
2 |
3 | import akka.actor.{ActorRef, ActorSystem}
4 | import kamon.Kamon
5 | import kamon.instrumentation.akka.AkkaInstrumentation.TrackRouterFilterName
6 | import kamon.instrumentation.akka.AkkaMetrics
7 | import kamon.instrumentation.akka.AkkaMetrics.RouterInstruments
8 |
9 | /**
10 | * Exposes the necessary callbacks for instrumenting a Router.
11 | */
12 | trait RouterMonitor {
13 |
14 | /**
15 | * Signals that a routee has been added to the router.
16 | */
17 | def routeeAdded(): Unit
18 |
19 | /**
20 | * Signals that a routee has been removed from the router.
21 | */
22 | def routeeRemoved(): Unit
23 |
24 | /**
25 | * Callback executed with processing message starts. In a router, the processing time is actually just about routing
26 | * the message to the right routee and there is no mailbox involved.
27 | */
28 | def processMessageStart(): Long
29 |
30 | /**
31 | * Callback executed when the message has been routed.
32 | */
33 | def processMessageEnd(timestampBeforeProcessing: Long): Unit
34 |
35 | /**
36 | * Callback executed when a router fails to route a message.
37 | */
38 | def processFailure(failure: Throwable): Unit
39 |
40 | /**
41 | * Cleans up all resources used by the router monitor.
42 | */
43 | def cleanup(): Unit
44 | }
45 |
46 | object RouterMonitor {
47 |
48 | def from(actorCell: Any, ref: ActorRef, parent: ActorRef, system: ActorSystem): RouterMonitor = {
49 | val cell = ActorCellInfo.from(actorCell, ref, parent, system)
50 |
51 | if (Kamon.filter(TrackRouterFilterName).accept(cell.path))
52 | new MetricsOnlyRouterMonitor(
53 | AkkaMetrics.forRouter(
54 | cell.path,
55 | cell.systemName,
56 | cell.dispatcherName,
57 | cell.actorOrRouterClass.getName,
58 | cell.routeeClass.map(_.getName).getOrElse("Unknown")
59 | )
60 | )
61 | else NoOpRouterMonitor
62 | }
63 |
64 | /**
65 | * Router monitor that doesn't perform any actions.
66 | */
67 | object NoOpRouterMonitor extends RouterMonitor {
68 | override def routeeAdded(): Unit = {}
69 | override def routeeRemoved(): Unit = {}
70 | override def processMessageStart(): Long = 0L
71 | override def processMessageEnd(timestampBeforeProcessing: Long): Unit = {}
72 | override def processFailure(failure: Throwable): Unit = {}
73 | override def cleanup(): Unit = {}
74 | }
75 |
76 | /**
77 | * Router monitor that tracks routing metrics for the router.
78 | */
79 | class MetricsOnlyRouterMonitor(routerMetrics: RouterInstruments) extends RouterMonitor {
80 | private val _clock = Kamon.clock()
81 |
82 | override def routeeAdded(): Unit = {}
83 | override def routeeRemoved(): Unit = {}
84 | override def processFailure(failure: Throwable): Unit = {}
85 |
86 | override def processMessageStart(): Long =
87 | _clock.nanos()
88 |
89 | override def processMessageEnd(timestampBeforeProcessing: Long): Unit = {
90 | val timestampAfterProcessing = _clock.nanos()
91 | val routingTime = timestampAfterProcessing - timestampBeforeProcessing
92 | routerMetrics.routingTime.record(routingTime)
93 | }
94 |
95 | override def cleanup(): Unit =
96 | routerMetrics.remove()
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/instrumentation/akka-2.5/src/main/protobuf/WireFormats.proto:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2009-2014 Typesafe Inc.
3 | */
4 |
5 | // Extracted from https://github.com/akka/akka/blob/master/akka-remote/src/main/protobuf/WireFormats.proto
6 |
7 | syntax = "proto2";
8 | option java_package = "akka.remote";
9 | option optimize_for = SPEED;
10 |
11 | /******************************************
12 | * Remoting message formats
13 | ******************************************/
14 |
15 |
16 | message AckAndEnvelopeContainer {
17 | optional AcknowledgementInfo ack = 1;
18 | optional RemoteEnvelope envelope = 2;
19 | }
20 |
21 | /**
22 | * Defines a remote message.
23 | */
24 | message RemoteEnvelope {
25 | required ActorRefData recipient = 1;
26 | required SerializedMessage message = 2;
27 | optional ActorRefData sender = 4;
28 | optional fixed64 seq = 5;
29 | }
30 |
31 | message AcknowledgementInfo {
32 | required fixed64 cumulativeAck = 1;
33 | repeated fixed64 nacks = 2;
34 | }
35 |
36 | /**
37 | * Defines a remote ActorRef that "remembers" and uses its original Actor instance
38 | * on the original node.
39 | */
40 | message ActorRefData {
41 | required string path = 1;
42 | }
43 |
44 | /**
45 | * Defines a message.
46 | */
47 | message SerializedMessage {
48 | required bytes message = 1;
49 | required int32 serializerId = 2;
50 | optional bytes messageManifest = 3;
51 | }
52 |
53 | /**
54 | * Defines akka.remote.DaemonMsgCreate
55 | */
56 | message DaemonMsgCreateData {
57 | required PropsData props = 1;
58 | required DeployData deploy = 2;
59 | required string path = 3;
60 | required ActorRefData supervisor = 4;
61 | }
62 |
63 | /**
64 | * Serialization of akka.actor.Props
65 | */
66 | message PropsData {
67 | required DeployData deploy = 2;
68 | required string clazz = 3;
69 | repeated bytes args = 4;
70 | repeated string classes = 5;
71 | }
72 |
73 | /**
74 | * Serialization of akka.actor.Deploy
75 | */
76 | message DeployData {
77 | required string path = 1;
78 | optional bytes config = 2;
79 | optional bytes routerConfig = 3;
80 | optional bytes scope = 4;
81 | optional string dispatcher = 5;
82 | }
83 |
84 |
85 | /******************************************
86 | * Akka Protocol message formats
87 | ******************************************/
88 |
89 | /**
90 | * Message format of Akka Protocol.
91 | * Message contains either a payload or an instruction.
92 | */
93 | message AkkaProtocolMessage {
94 | optional bytes payload = 1;
95 | optional AkkaControlMessage instruction = 2;
96 | }
97 |
98 | /**
99 | * Defines some control messages for the remoting
100 | */
101 | message AkkaControlMessage {
102 | required CommandType commandType = 1;
103 | optional AkkaHandshakeInfo handshakeInfo = 2;
104 | }
105 |
106 | message AkkaHandshakeInfo {
107 | required AddressData origin = 1;
108 | required fixed64 uid = 2;
109 | optional string cookie = 3;
110 |
111 | }
112 |
113 | /**
114 | * Defines the type of the AkkaControlMessage command type
115 | */
116 | enum CommandType {
117 | ASSOCIATE = 1;
118 | DISASSOCIATE = 2;
119 | HEARTBEAT = 3;
120 | DISASSOCIATE_SHUTTING_DOWN = 4; // Remote system is going down and will not accepts new connections
121 | DISASSOCIATE_QUARANTINED = 5; // Remote system refused the association since the current system is quarantined
122 | }
123 |
124 | /**
125 | * Defines a remote address.
126 | */
127 | message AddressData {
128 | required string system = 1;
129 | required string hostname = 2;
130 | required uint32 port = 3;
131 | optional string protocol = 4;
132 | }
--------------------------------------------------------------------------------
/instrumentation/akka-2.6/src/main/protobuf/WireFormats.proto:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2009-2014 Typesafe Inc.
3 | */
4 |
5 | // Extracted from https://github.com/akka/akka/blob/master/akka-remote/src/main/protobuf/WireFormats.proto
6 |
7 | syntax = "proto2";
8 | option java_package = "akka.remote";
9 | option optimize_for = SPEED;
10 |
11 | /******************************************
12 | * Remoting message formats
13 | ******************************************/
14 |
15 |
16 | message AckAndEnvelopeContainer {
17 | optional AcknowledgementInfo ack = 1;
18 | optional RemoteEnvelope envelope = 2;
19 | }
20 |
21 | /**
22 | * Defines a remote message.
23 | */
24 | message RemoteEnvelope {
25 | required ActorRefData recipient = 1;
26 | required SerializedMessage message = 2;
27 | optional ActorRefData sender = 4;
28 | optional fixed64 seq = 5;
29 | }
30 |
31 | message AcknowledgementInfo {
32 | required fixed64 cumulativeAck = 1;
33 | repeated fixed64 nacks = 2;
34 | }
35 |
36 | /**
37 | * Defines a remote ActorRef that "remembers" and uses its original Actor instance
38 | * on the original node.
39 | */
40 | message ActorRefData {
41 | required string path = 1;
42 | }
43 |
44 | /**
45 | * Defines a message.
46 | */
47 | message SerializedMessage {
48 | required bytes message = 1;
49 | required int32 serializerId = 2;
50 | optional bytes messageManifest = 3;
51 | }
52 |
53 | /**
54 | * Defines akka.remote.DaemonMsgCreate
55 | */
56 | message DaemonMsgCreateData {
57 | required PropsData props = 1;
58 | required DeployData deploy = 2;
59 | required string path = 3;
60 | required ActorRefData supervisor = 4;
61 | }
62 |
63 | /**
64 | * Serialization of akka.actor.Props
65 | */
66 | message PropsData {
67 | required DeployData deploy = 2;
68 | required string clazz = 3;
69 | repeated bytes args = 4;
70 | repeated string classes = 5;
71 | }
72 |
73 | /**
74 | * Serialization of akka.actor.Deploy
75 | */
76 | message DeployData {
77 | required string path = 1;
78 | optional bytes config = 2;
79 | optional bytes routerConfig = 3;
80 | optional bytes scope = 4;
81 | optional string dispatcher = 5;
82 | }
83 |
84 |
85 | /******************************************
86 | * Akka Protocol message formats
87 | ******************************************/
88 |
89 | /**
90 | * Message format of Akka Protocol.
91 | * Message contains either a payload or an instruction.
92 | */
93 | message AkkaProtocolMessage {
94 | optional bytes payload = 1;
95 | optional AkkaControlMessage instruction = 2;
96 | }
97 |
98 | /**
99 | * Defines some control messages for the remoting
100 | */
101 | message AkkaControlMessage {
102 | required CommandType commandType = 1;
103 | optional AkkaHandshakeInfo handshakeInfo = 2;
104 | }
105 |
106 | message AkkaHandshakeInfo {
107 | required AddressData origin = 1;
108 | required fixed64 uid = 2;
109 | optional string cookie = 3;
110 |
111 | }
112 |
113 | /**
114 | * Defines the type of the AkkaControlMessage command type
115 | */
116 | enum CommandType {
117 | ASSOCIATE = 1;
118 | DISASSOCIATE = 2;
119 | HEARTBEAT = 3;
120 | DISASSOCIATE_SHUTTING_DOWN = 4; // Remote system is going down and will not accepts new connections
121 | DISASSOCIATE_QUARANTINED = 5; // Remote system refused the association since the current system is quarantined
122 | }
123 |
124 | /**
125 | * Defines a remote address.
126 | */
127 | message AddressData {
128 | required string system = 1;
129 | required string hostname = 2;
130 | required uint32 port = 3;
131 | optional string protocol = 4;
132 | }
--------------------------------------------------------------------------------
/instrumentation/common/src/main/scala/kamon/instrumentation/akka/instrumentations/ActorCellInfo.scala:
--------------------------------------------------------------------------------
1 | package kamon.instrumentation.akka.instrumentations
2 |
3 | import akka.actor.{ActorRef, ActorSystem, Props}
4 | import akka.routing.{BalancingPool, NoRouter}
5 |
6 | import scala.language.existentials
7 |
8 | /**
9 | * Basic information that should be read from an ActorCell for instrumentation purposes.
10 | */
11 | case class ActorCellInfo (
12 | path: String,
13 | name: String,
14 | systemName: String,
15 | dispatcherName: String,
16 | isRouter: Boolean,
17 | isRoutee: Boolean,
18 | isRootSupervisor: Boolean,
19 | isTemporary: Boolean,
20 | actorOrRouterClass: Class[_],
21 | routeeClass: Option[Class[_]]
22 | )
23 |
24 | object ActorCellInfo {
25 |
26 | /**
27 | * Reads information from an ActorCell.
28 | */
29 | def from(cell: Any, ref: ActorRef, parent: ActorRef, system: ActorSystem): ActorCellInfo = {
30 | val props = AkkaPrivateAccess.cellProps(cell).get
31 | val actorName = ref.path.name
32 |
33 | val pathString = ref.path.elements.mkString("/")
34 | val isRootSupervisor = pathString.length == 0 || pathString == "user" || pathString == "system"
35 | val isRouter = hasRouterProps(props)
36 | val isRoutee = AkkaPrivateAccess.isRoutedActorRef(parent)
37 | val isTemporary = AkkaPrivateAccess.isUnstartedActorCell(cell)
38 |
39 | val (actorOrRouterClass, routeeClass) =
40 | if(isRouter)
41 | (props.routerConfig.getClass, Some(ref.asInstanceOf[HasRouterProps].routeeProps.actorClass))
42 | else if (isRoutee)
43 | (parent.asInstanceOf[HasRouterProps].routerProps.routerConfig.getClass, Some(props.actorClass))
44 | else
45 | (props.actorClass(), None)
46 |
47 | val fullPath = if (isRoutee) cellName(system, parent) else cellName(system, ref)
48 | val dispatcherName = if(isRouter) {
49 | if(props.routerConfig.isInstanceOf[BalancingPool]) {
50 |
51 | // Even though the router actor for a BalancingPool can have a different dispatcher we will
52 | // assign the name of the same dispatcher where the routees will run to ensure all metrics are
53 | // correlated and cleaned up correctly.
54 | val deployPath = ref.path.elements.drop(1).mkString("/", "/", "")
55 | "BalancingPool-" + deployPath
56 |
57 | } else {
58 |
59 | // It might happen that the deployment configuration will provide a different dispatcher name
60 | // for the routees and we should catch that case only when creating the router (the routees will
61 | // be initialized with an updated Props instance.
62 | AkkaPrivateAccess.lookupDeploy(ref.path, system).map(_.dispatcher).getOrElse(props.dispatcher)
63 | }
64 | } else props.dispatcher
65 |
66 | ActorCellInfo(fullPath, actorName, system.name, dispatcherName, isRouter, isRoutee, isRootSupervisor, isTemporary,
67 | actorOrRouterClass, routeeClass)
68 | }
69 |
70 | /**
71 | * Returns a simple Class name, working around issues that might arise when using double nested classes in Scala.
72 | */
73 | def simpleClassName(cls: Class[_]): String = {
74 | // Class.getSimpleName could fail if called on a double-nested class.
75 | // See https://github.com/scala/bug/issues/2034 for more details.
76 | try { cls.getSimpleName } catch { case _: Throwable => {
77 | val className = cls.getName
78 | val lastSeparator = className.lastIndexOf('.')
79 |
80 | if(lastSeparator > 0)
81 | className.substring(lastSeparator + 1)
82 | else
83 | className
84 | }}
85 | }
86 |
87 | private def hasRouterProps(props: Props): Boolean =
88 | props.deploy.routerConfig != NoRouter
89 |
90 | private def cellName(system: ActorSystem, ref: ActorRef): String =
91 | system.name + "/" + ref.path.elements.mkString("/")
92 | }
93 |
--------------------------------------------------------------------------------
/test/common/src/test/scala/kamon/instrumentation/akka/AskPatternInstrumentationSpec.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * =========================================================================================
3 | * Copyright © 2013 the kamon project
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
6 | * except in compliance with the License. You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software distributed under the
11 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
12 | * either express or implied. See the License for the specific language governing permissions
13 | * and limitations under the License.
14 | * =========================================================================================
15 | */
16 |
17 | package kamon.instrumentation.akka
18 |
19 |
20 | import akka.actor._
21 | import akka.pattern.ask
22 | import akka.testkit.{EventFilter, ImplicitSender, TestKit}
23 | import akka.util.Timeout
24 | import com.typesafe.config.ConfigFactory
25 | import kamon.Kamon
26 | import org.scalatest.{BeforeAndAfterAll, WordSpecLike}
27 | import ContextTesting._
28 |
29 | import scala.concurrent.duration._
30 |
31 | class AskPatternInstrumentationSpec extends TestKit(ActorSystem("AskPatternInstrumentationSpec")) with WordSpecLike with BeforeAndAfterAll with ImplicitSender {
32 |
33 | implicit lazy val ec = system.dispatcher
34 | implicit val askTimeout = Timeout(10 millis)
35 |
36 | // TODO: Make this work with ActorSelections
37 |
38 | "the AskPatternInstrumentation" when {
39 | "configured in heavyweight mode" should {
40 | "log a warning with a full stack trace and the context captured the moment when the ask was triggered for an actor" in {
41 | val noReplyActorRef = system.actorOf(Props[NoReply], "no-reply-1")
42 | setAskPatternTimeoutWarningMode("heavyweight")
43 |
44 | EventFilter.warning(start = "Timeout triggered for ask pattern to actor [no-reply-1] at").intercept {
45 | Kamon.runWithContext(testContext("ask-timeout-warning")) {
46 | noReplyActorRef ? "hello"
47 | }
48 | }
49 | }
50 | }
51 |
52 | "configured in lightweight mode" should {
53 | "log a warning with a short source location description and the context taken from the moment the ask was triggered for a actor" in {
54 | val noReplyActorRef = system.actorOf(Props[NoReply], "no-reply-2")
55 | setAskPatternTimeoutWarningMode("lightweight")
56 |
57 | EventFilter.warning(start = "Timeout triggered for ask pattern to actor [no-reply-2] at").intercept {
58 | Kamon.runWithContext(testContext("ask-timeout-warning")) {
59 | noReplyActorRef ? "hello"
60 | }
61 | }
62 | }
63 | }
64 |
65 | "configured in off mode" should {
66 | "should not log any warning messages" in {
67 | val noReplyActorRef = system.actorOf(Props[NoReply], "no-reply-3")
68 | setAskPatternTimeoutWarningMode("off")
69 |
70 | intercept[AssertionError] { // No message will be logged and the event filter will fail.
71 | EventFilter.warning(start = "Timeout triggered for ask pattern to actor", occurrences = 1).intercept {
72 | Kamon.runWithContext(testContext("ask-timeout-warning")) {
73 | noReplyActorRef ? "hello"
74 | }
75 | }
76 | }
77 | }
78 | }
79 | }
80 |
81 | override protected def afterAll(): Unit = shutdown()
82 |
83 | def setAskPatternTimeoutWarningMode(mode: String): Unit = {
84 | val newConfiguration = ConfigFactory.parseString(s"kamon.akka.ask-pattern-timeout-warning=$mode").withFallback(Kamon.config())
85 | Kamon.reconfigure(newConfiguration)
86 | }
87 | }
88 |
89 | class NoReply extends Actor {
90 | def receive = {
91 | case _ =>
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/instrumentation/akka-2.5/src/main/scala/kamon/instrumentation/akka/instrumentations/akka_25/remote/internal/AkkaPduProtobufCodecConstructMessageMethodInterceptor.scala:
--------------------------------------------------------------------------------
1 | package akka.kamon.instrumentation.akka.instrumentations.akka_25.remote
2 |
3 | import java.io.ByteArrayOutputStream
4 |
5 | import akka.KamonOptionVal.OptionVal
6 | import akka.actor.{ActorRef, Address}
7 | import akka.remote.ContextAwareWireFormats.{AckAndContextAwareEnvelopeContainer, ContextAwareRemoteEnvelope, RemoteContext}
8 | import akka.remote.WireFormats.{AcknowledgementInfo, ActorRefData, AddressData, SerializedMessage}
9 | import akka.remote.{Ack, SeqNo}
10 | import akka.util.ByteString
11 | import kamon.Kamon
12 | import kamon.context.BinaryPropagation.ByteStreamWriter
13 | import kamon.instrumentation.akka.AkkaRemoteMetrics
14 | import kanela.agent.libs.net.bytebuddy.implementation.bind.annotation.{Argument, RuntimeType}
15 |
16 | /**
17 | * Interceptor for akka.remote.transport.AkkaPduProtobufCodec$::constructMessage
18 | */
19 | class AkkaPduProtobufCodecConstructMessageMethodInterceptor {
20 |
21 | @RuntimeType
22 | def aroundConstructMessage(@Argument(0) localAddress: Address,
23 | @Argument(1) recipient: ActorRef,
24 | @Argument(2) serializedMessage: SerializedMessage,
25 | @Argument(3) senderOption: OptionVal[ActorRef],
26 | @Argument(4) seqOption: Option[SeqNo],
27 | @Argument(5) ackOption: Option[Ack]): AnyRef = {
28 |
29 | val ackAndEnvelopeBuilder = AckAndContextAwareEnvelopeContainer.newBuilder
30 | val envelopeBuilder = ContextAwareRemoteEnvelope.newBuilder
31 |
32 | envelopeBuilder.setRecipient(serializeActorRef(recipient.path.address, recipient))
33 | if (senderOption.isDefined)
34 | envelopeBuilder.setSender(serializeActorRef(localAddress, senderOption.get))
35 | seqOption foreach { seq => envelopeBuilder.setSeq(seq.rawValue) }
36 | ackOption foreach { ack => ackAndEnvelopeBuilder.setAck(ackBuilder(ack)) }
37 | envelopeBuilder.setMessage(serializedMessage)
38 |
39 | val out = new ByteArrayOutputStream()
40 | Kamon.defaultBinaryPropagation().write(Kamon.currentContext(), ByteStreamWriter.of(out))
41 |
42 | val remoteTraceContext = RemoteContext.newBuilder().setContext(
43 | akka.protobuf.ByteString.copyFrom(out.toByteArray)
44 | )
45 | envelopeBuilder.setTraceContext(remoteTraceContext)
46 |
47 | ackAndEnvelopeBuilder.setEnvelope(envelopeBuilder)
48 |
49 | val messageSize = envelopeBuilder.getMessage.getMessage.size()
50 | AkkaRemoteMetrics.serializationInstruments(localAddress.system).outboundMessageSize.record(messageSize)
51 |
52 | ByteString.ByteString1C(ackAndEnvelopeBuilder.build.toByteArray) //Reuse Byte Array (naughty!)
53 | }
54 |
55 | // Copied from akka.remote.transport.AkkaPduProtobufCodec because of private access.
56 | private def ackBuilder(ack: Ack): AcknowledgementInfo.Builder = {
57 | val ackBuilder = AcknowledgementInfo.newBuilder()
58 | ackBuilder.setCumulativeAck(ack.cumulativeAck.rawValue)
59 | ack.nacks foreach { nack => ackBuilder.addNacks(nack.rawValue) }
60 | ackBuilder
61 | }
62 |
63 | // Copied from akka.remote.transport.AkkaPduProtobufCodec because of private access.
64 | private def serializeActorRef(defaultAddress: Address, ref: ActorRef): ActorRefData = {
65 | ActorRefData.newBuilder.setPath(
66 | if (ref.path.address.host.isDefined) ref.path.toSerializationFormat
67 | else ref.path.toSerializationFormatWithAddress(defaultAddress)).build()
68 | }
69 |
70 | // Copied from akka.remote.transport.AkkaPduProtobufCodec because of private access.
71 | private def serializeAddress(address: Address): AddressData = address match {
72 | case Address(protocol, system, Some(host), Some(port)) =>
73 | AddressData.newBuilder
74 | .setHostname(host)
75 | .setPort(port)
76 | .setSystem(system)
77 | .setProtocol(protocol)
78 | .build()
79 | case _ => throw new IllegalArgumentException(s"Address [$address] could not be serialized: host or port missing.")
80 | }
81 | }
--------------------------------------------------------------------------------
/instrumentation/akka-2.6/src/main/scala/kamon/instrumentation/akka/instrumentations/akka_26/remote/internal/AkkaPduProtobufCodecConstructMessageMethodInterceptor.scala:
--------------------------------------------------------------------------------
1 | package akka.kamon.instrumentation.akka.instrumentations.akka_26.remote.internal
2 |
3 | import java.io.ByteArrayOutputStream
4 |
5 | import akka.KamonOptionVal.OptionVal
6 | import akka.actor.{ActorRef, Address}
7 | import akka.remote.ContextAwareWireFormats_Akka26.{AckAndContextAwareEnvelopeContainer, ContextAwareRemoteEnvelope, RemoteContext}
8 | import akka.remote.WireFormats.{AcknowledgementInfo, ActorRefData, AddressData, SerializedMessage}
9 | import akka.remote.{Ack, SeqNo}
10 | import akka.util.ByteString
11 | import kamon.Kamon
12 | import kamon.context.BinaryPropagation.ByteStreamWriter
13 | import kamon.instrumentation.akka.AkkaRemoteMetrics
14 | import kanela.agent.libs.net.bytebuddy.implementation.bind.annotation.{Argument, RuntimeType}
15 |
16 | /**
17 | * Interceptor for akka.remote.transport.AkkaPduProtobufCodec$::constructMessage
18 | */
19 | class AkkaPduProtobufCodecConstructMessageMethodInterceptor {
20 |
21 | @RuntimeType
22 | def aroundConstructMessage(@Argument(0) localAddress: Address,
23 | @Argument(1) recipient: ActorRef,
24 | @Argument(2) serializedMessage: SerializedMessage,
25 | @Argument(3) senderOption: OptionVal[ActorRef],
26 | @Argument(4) seqOption: Option[SeqNo],
27 | @Argument(5) ackOption: Option[Ack]): AnyRef = {
28 |
29 | val ackAndEnvelopeBuilder = AckAndContextAwareEnvelopeContainer.newBuilder
30 | val envelopeBuilder = ContextAwareRemoteEnvelope.newBuilder
31 |
32 | envelopeBuilder.setRecipient(serializeActorRef(recipient.path.address, recipient))
33 | if (senderOption.isDefined)
34 | envelopeBuilder.setSender(serializeActorRef(localAddress, senderOption.get))
35 | seqOption foreach { seq => envelopeBuilder.setSeq(seq.rawValue) }
36 | ackOption foreach { ack => ackAndEnvelopeBuilder.setAck(ackBuilder(ack)) }
37 | envelopeBuilder.setMessage(serializedMessage)
38 |
39 | val out = new ByteArrayOutputStream()
40 | Kamon.defaultBinaryPropagation().write(Kamon.currentContext(), ByteStreamWriter.of(out))
41 |
42 | val remoteTraceContext = RemoteContext.newBuilder().setContext(
43 | akka.protobufv3.internal.ByteString.copyFrom(out.toByteArray)
44 | )
45 | envelopeBuilder.setTraceContext(remoteTraceContext)
46 |
47 | ackAndEnvelopeBuilder.setEnvelope(envelopeBuilder)
48 |
49 | val messageSize = envelopeBuilder.getMessage.getMessage.size()
50 | AkkaRemoteMetrics.serializationInstruments(localAddress.system).outboundMessageSize.record(messageSize)
51 |
52 | ByteString.ByteString1C(ackAndEnvelopeBuilder.build.toByteArray) //Reuse Byte Array (naughty!)
53 | }
54 |
55 | // Copied from akka.remote.transport.AkkaPduProtobufCodec because of private access.
56 | private def ackBuilder(ack: Ack): AcknowledgementInfo.Builder = {
57 | val ackBuilder = AcknowledgementInfo.newBuilder()
58 | ackBuilder.setCumulativeAck(ack.cumulativeAck.rawValue)
59 | ack.nacks foreach { nack => ackBuilder.addNacks(nack.rawValue) }
60 | ackBuilder
61 | }
62 |
63 | // Copied from akka.remote.transport.AkkaPduProtobufCodec because of private access.
64 | private def serializeActorRef(defaultAddress: Address, ref: ActorRef): ActorRefData = {
65 | ActorRefData.newBuilder.setPath(
66 | if (ref.path.address.host.isDefined) ref.path.toSerializationFormat
67 | else ref.path.toSerializationFormatWithAddress(defaultAddress)).build()
68 | }
69 |
70 | // Copied from akka.remote.transport.AkkaPduProtobufCodec because of private access.
71 | private def serializeAddress(address: Address): AddressData = address match {
72 | case Address(protocol, system, Some(host), Some(port)) =>
73 | AddressData.newBuilder
74 | .setHostname(host)
75 | .setPort(port)
76 | .setSystem(system)
77 | .setProtocol(protocol)
78 | .build()
79 | case _ => throw new IllegalArgumentException(s"Address [$address] could not be serialized: host or port missing.")
80 | }
81 | }
--------------------------------------------------------------------------------
/test/common/src/test/resources/application.conf:
--------------------------------------------------------------------------------
1 | akka {
2 | loglevel = INFO
3 | loggers = [ "akka.event.slf4j.Slf4jLogger" ]
4 | logger-startup-timeout = 30s
5 | log-dead-letters = 0
6 |
7 | actor {
8 | serialize-messages = on
9 | warn-about-java-serializer-usage = no
10 |
11 | deployment {
12 | /picking-the-right-dispatcher-in-pool-router {
13 | router = round-robin-pool
14 | resizer = {
15 | lower-bound = 5
16 | upper-bound = 64
17 | messages-per-resize = 20
18 | }
19 | }
20 |
21 | "/picking-the-right-dispatcher-in-pool-router/*" {
22 | dispatcher = custom-dispatcher
23 | }
24 | }
25 | }
26 | }
27 |
28 | custom-dispatcher {
29 | executor = "thread-pool-executor"
30 | type = PinnedDispatcher
31 | }
32 |
33 | tracked-pinned-dispatcher {
34 | executor = "thread-pool-executor"
35 | type = PinnedDispatcher
36 | }
37 |
38 | kamon {
39 | instrumentation.akka {
40 | filters {
41 |
42 | actors {
43 | track {
44 | includes = [ "*/user/tracked-*", "*/user/measuring-*", "*/user/clean-after-collect", "*/user/stop", "*/user/repointable*", "*/" ]
45 | excludes = [ "*/system/**", "*/user/tracked-explicitly-excluded", "*/user/non-tracked-actor" ]
46 | }
47 |
48 | trace {
49 | excludes = [ "*/user/filteredout*" ]
50 | }
51 |
52 | start-trace {
53 | includes = [ "*/user/traced*" ]
54 | excludes = []
55 | }
56 | }
57 |
58 | routers {
59 | includes = [ "*/user/tracked-*", "*/user/measuring-*", "*/user/cleanup-*", "*/user/picking-*", "*/user/stop-*" ]
60 | excludes = [ "*/user/tracked-explicitly-excluded-*"]
61 | }
62 |
63 | dispatchers {
64 | includes = [ "**" ]
65 | excludes = [ "explicitly-excluded" ]
66 | }
67 |
68 | groups {
69 | auto-grouping {
70 | excludes = [ "*/user/ActorMetricsTestActor", "*/user/SecondLevelGrouping"]
71 | }
72 |
73 | group-of-actors {
74 | includes = ["*/user/group-of-actors-*"]
75 | excludes = []
76 | }
77 |
78 | second-level-group {
79 | includes = ["*/user/second-level-group/*"]
80 | }
81 |
82 | group-of-routees {
83 | includes = ["*/user/group-of-routees*"]
84 | excludes = []
85 | }
86 |
87 | }
88 | }
89 |
90 | cluster-sharding.shard-metrics-sample-interval = 100 millisecond
91 | }
92 |
93 | metric {
94 | tick-interval = 1 hour
95 |
96 | factory {
97 | default-settings {
98 | range-sampler.auto-update-interval = 10 millis
99 | }
100 |
101 | custom-settings {
102 | "akka.actor.mailbox-size" {
103 | auto-update-interval = 1 millisecond
104 | }
105 |
106 | "akka.group.members" {
107 | auto-update-interval = 1 millisecond
108 | }
109 | }
110 |
111 | }
112 | }
113 |
114 | trace.sampler = "always"
115 | }
116 |
117 | explicitly-excluded {
118 | type = "Dispatcher"
119 | executor = "fork-join-executor"
120 | }
121 |
122 | tracked-fjp {
123 | type = "Dispatcher"
124 | executor = "fork-join-executor"
125 |
126 | fork-join-executor {
127 | parallelism-min = 8
128 | parallelism-factor = 100.0
129 | parallelism-max = 22
130 | }
131 | }
132 |
133 | tracked-tpe {
134 | type = "Dispatcher"
135 | executor = "thread-pool-executor"
136 |
137 | thread-pool-executor {
138 | core-pool-size-min = 7
139 | core-pool-size-factor = 100.0
140 | max-pool-size-factor = 100.0
141 | max-pool-size-max = 21
142 | core-pool-size-max = 21
143 | }
144 | }
145 |
146 |
147 | kanela.modules.akka-testkit {
148 | name = "Akka Testkit Instrumentation"
149 | description = "Delays messages received by the Test Kit Actors to give enough time for other Threads to finish their work"
150 |
151 | instrumentations = [
152 | "kamon.instrumentation.akka.AkkaTestKitInstrumentation"
153 | ]
154 |
155 | within = [
156 | "^akka.testkit.*"
157 | ]
158 | }
--------------------------------------------------------------------------------
/instrumentation/common/src/main/scala/kamon/instrumentation/akka/instrumentations/AskPatternInstrumentation.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * =========================================================================================
3 | * Copyright © 2013-2018 the kamon project
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
6 | * except in compliance with the License. You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software distributed under the
11 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
12 | * either express or implied. See the License for the specific language governing permissions
13 | * and limitations under the License.
14 | * =========================================================================================
15 | */
16 |
17 | package kamon.instrumentation.akka.instrumentations
18 |
19 | import akka.actor.ActorRef
20 | import akka.pattern.AskTimeoutException
21 | import akka.util.Timeout
22 | import kamon.Kamon
23 | import kamon.instrumentation.akka.AkkaInstrumentation
24 | import kamon.instrumentation.akka.AkkaInstrumentation.AskPatternTimeoutWarningSetting.{Heavyweight, Lightweight, Off}
25 | import kamon.util.CallingThreadExecutionContext
26 | import kanela.agent.api.instrumentation.InstrumentationBuilder
27 | import kanela.agent.libs.net.bytebuddy.asm.Advice.{Argument, OnMethodExit, Origin, Return}
28 | import org.slf4j.LoggerFactory
29 |
30 | import scala.compat.Platform.EOL
31 | import scala.concurrent.Future
32 |
33 | class AskPatternInstrumentation extends InstrumentationBuilder {
34 |
35 | /**
36 | * Logs a warning message with various levels of detail when a Future[X] returned by the Ask pattern times out.
37 | */
38 | onType("akka.pattern.AskableActorRef$")
39 | .advise(method("$qmark$extension"), classOf[AskPatternInstrumentation])
40 |
41 | }
42 |
43 | object AskPatternInstrumentation {
44 |
45 | private val _log = LoggerFactory.getLogger(classOf[AskPatternInstrumentation])
46 |
47 | private class StackTraceCaptureException extends Throwable
48 |
49 | private case class SourceLocation (
50 | declaringType: String,
51 | method: String
52 | )
53 |
54 | @OnMethodExit(suppress = classOf[Throwable])
55 | def onExit(@Origin origin: String, @Return future: Future[AnyRef], @Argument(0) actor: ActorRef, @Argument(2) timeout: Timeout) = {
56 |
57 | if(AkkaPrivateAccess.isInternalAndActiveActorRef(actor) && Kamon.currentContext().nonEmpty()) {
58 | AkkaInstrumentation.settings().askPatternWarning match {
59 | case Off =>
60 | case Lightweight => hookLightweightWarning(future, sourceLocation(origin), actor)
61 | case Heavyweight => hookHeavyweightWarning(future, new StackTraceCaptureException, actor)
62 | }
63 |
64 | hookHeavyweightWarning(future, new StackTraceCaptureException, actor)
65 | }
66 | }
67 |
68 | private def ifAskTimeoutException(code: => Unit): PartialFunction[Throwable, Unit] = {
69 | case _: AskTimeoutException => code
70 | case _ =>
71 | }
72 |
73 |
74 | private def hookLightweightWarning(future: Future[AnyRef], sourceLocation: SourceLocation, actor: ActorRef): Unit = {
75 | val locationString = Option(sourceLocation)
76 | .map(location => s"${location.declaringType}:${location.method}")
77 | .getOrElse("")
78 |
79 | future.failed.foreach(ifAskTimeoutException {
80 | _log.warn(s"Timeout triggered for ask pattern to actor [${actor.path.name}] at [$locationString]")
81 | })(CallingThreadExecutionContext)
82 | }
83 |
84 | private def hookHeavyweightWarning(future: Future[AnyRef], captureException: StackTraceCaptureException, actor: ActorRef): Unit = {
85 | val locationString = captureException.getStackTrace.drop(3).mkString("", System.lineSeparator, System.lineSeparator)
86 |
87 | future.failed.foreach(ifAskTimeoutException {
88 | _log.warn(s"Timeout triggered for ask pattern to actor [${actor.path.name}] at [$locationString]")
89 | })(CallingThreadExecutionContext)
90 | }
91 |
92 | private def sourceLocation(origin: String): SourceLocation = {
93 | val methodDescription = origin.split(" ")
94 | SourceLocation(methodDescription(0), methodDescription(1))
95 | }
96 | }
--------------------------------------------------------------------------------
/test/common/src/test/scala/kamon/instrumentation/akka/ActorSystemMetricsSpec.scala:
--------------------------------------------------------------------------------
1 | /* =========================================================================================
2 | * Copyright © 2013 the kamon project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5 | * except in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the
10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
11 | * either express or implied. See the License for the specific language governing permissions
12 | * and limitations under the License.
13 | * =========================================================================================
14 | */
15 |
16 | package kamon.instrumentation.akka
17 |
18 | import akka.Version
19 | import akka.actor._
20 | import akka.testkit.{ImplicitSender, TestKit, TestProbe}
21 | import kamon.instrumentation.akka.ActorMetricsTestActor._
22 | import kamon.testkit.{InstrumentInspection, MetricInspection}
23 | import org.scalactic.TimesOnInt._
24 | import org.scalatest.concurrent.Eventually
25 | import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike}
26 |
27 | import scala.concurrent.duration._
28 |
29 |
30 | class ActorSystemMetricsSpec extends TestKit(ActorSystem("ActorSystemMetricsSpec")) with WordSpecLike with MetricInspection.Syntax with InstrumentInspection.Syntax with Matchers
31 | with BeforeAndAfterAll with ImplicitSender with Eventually {
32 |
33 | // Akka 2.6 creates two more actors by default for the streams materializers supervisor.
34 | val baseActorCount = if(Version.current.startsWith("2.6")) 8L else 6L
35 | val systemMetrics = AkkaMetrics.forSystem(system.name)
36 |
37 | "the Actor System metrics" should {
38 | "record active actor counts" in {
39 | eventually(timeout(5 seconds)) {
40 | val activeActors = systemMetrics.activeActors.distribution()
41 |
42 | // This establishes a baseline on actor counts for the rest of the test.
43 | activeActors.count should be > 0L
44 | activeActors.min shouldBe baseActorCount
45 | activeActors.max shouldBe baseActorCount
46 | }
47 |
48 | val actors = (1 to 10).map(id => watch(system.actorOf(Props[ActorMetricsTestActor], s"just-some-actor-$id")))
49 | val parent = watch(system.actorOf(Props[SecondLevelGrouping], "just-some-parent-actor"))
50 |
51 | 1000 times {
52 | actors.foreach(_ ! Discard)
53 | parent ! Discard
54 | }
55 |
56 | eventually(timeout(5 seconds)) {
57 | val activeActors = systemMetrics.activeActors.distribution()
58 | activeActors.count should be > 0L
59 | activeActors.min shouldBe 21L + baseActorCount
60 | activeActors.max shouldBe 21L + baseActorCount
61 | }
62 |
63 | actors.foreach(system.stop)
64 | system.stop(parent)
65 |
66 | eventually(timeout(5 seconds)) {
67 | val activeActors = systemMetrics.activeActors.distribution()
68 | activeActors.count should be > 0L
69 | activeActors.min shouldBe baseActorCount
70 | activeActors.max shouldBe baseActorCount
71 | }
72 | }
73 |
74 | "record dead letters" in {
75 | val doaActor = system.actorOf(Props[ActorMetricsTestActor], "doa")
76 | val deathWatcher = TestProbe()
77 | systemMetrics.deadLetters.value(true)
78 | deathWatcher.watch(doaActor)
79 | doaActor ! PoisonPill
80 | deathWatcher.expectTerminated(doaActor)
81 |
82 | 7 times { doaActor ! "deadonarrival" }
83 |
84 | eventually {
85 | systemMetrics.deadLetters.value(false).toInt should be(7)
86 | }
87 | }
88 |
89 | "record unhandled messages" in {
90 | val unhandled = system.actorOf(Props[ActorMetricsTestActor], "unhandled")
91 | 10 times { unhandled ! "CantHandleStrings" }
92 |
93 | eventually {
94 | systemMetrics.unhandledMessages.value(false).toInt should be(10)
95 | }
96 | }
97 |
98 | "record processed messages counts" in {
99 | systemMetrics.processedMessagesByTracked.value(true)
100 | systemMetrics.processedMessagesByNonTracked.value(true)
101 | systemMetrics.processedMessagesByNonTracked.value(false) should be(0)
102 |
103 | val tracked = system.actorOf(Props[ActorMetricsTestActor], "tracked-actor-counts")
104 | val nonTracked = system.actorOf(Props[ActorMetricsTestActor], "non-tracked-actor-counts")
105 |
106 | (1 to 10).foreach(_ => tracked ! Discard)
107 | (1 to 15).foreach(_ => nonTracked ! Discard)
108 |
109 | eventually(timeout(3 second)) {
110 | systemMetrics.processedMessagesByTracked.value(false) should be >= (10L)
111 | systemMetrics.processedMessagesByNonTracked.value(false) should be >= (15L)
112 | }
113 | }
114 | }
115 |
116 | override protected def afterAll(): Unit = shutdown()
117 | }
118 |
--------------------------------------------------------------------------------
/instrumentation/common/src/main/scala/kamon/instrumentation/akka/AkkaClusterShardingMetrics.scala:
--------------------------------------------------------------------------------
1 | package kamon.instrumentation.akka
2 |
3 | import java.util.concurrent.{ScheduledFuture, TimeUnit}
4 | import java.util.concurrent.atomic.AtomicLong
5 |
6 | import kamon.{AtomicGetOrElseUpdateOnTrieMap, Kamon}
7 | import kamon.metric.{Histogram, InstrumentGroup}
8 | import kamon.tag.TagSet
9 |
10 | import scala.collection.concurrent.TrieMap
11 |
12 | object AkkaClusterShardingMetrics {
13 |
14 | val RegionHostedShards = Kamon.rangeSampler (
15 | name = "akka.cluster.sharding.region.hosted-shards",
16 | description = "Tracks the number of shards hosted by a region"
17 | )
18 |
19 | val RegionHostedEntities = Kamon.rangeSampler (
20 | name = "akka.cluster.sharding.region.hosted-entities",
21 | description = "Tracks the number of entities hosted by a region"
22 | )
23 |
24 | val RegionProcessedMessages = Kamon.counter (
25 | name = "akka.cluster.sharding.region.processed-messages",
26 | description = "Counts the number of messages processed by a region"
27 | )
28 |
29 | val ShardHostedEntities = Kamon.histogram (
30 | name = "akka.cluster.sharding.shard.hosted-entities",
31 | description = "Tracks the distribution of entity counts hosted per shard"
32 | )
33 |
34 | val ShardProcessedMessages = Kamon.histogram (
35 | name = "akka.cluster.sharding.shard.processed-messages",
36 | description = "Tracks the distribution of processed messages per shard"
37 | )
38 |
39 | class ShardingInstruments(system: String, typeName: String) extends InstrumentGroup(TagSet.of("type", typeName).withTag("system", system)) {
40 |
41 | val hostedShards = register(RegionHostedShards)
42 | val hostedEntities = register(RegionHostedEntities)
43 | val processedMessages = register(RegionProcessedMessages)
44 | val shardHostedEntities = register(ShardHostedEntities)
45 | val shardProcessedMessages = register(ShardProcessedMessages)
46 |
47 | private val _shardTelemetry = ShardingInstruments.shardTelemetry(system, typeName, shardHostedEntities, shardProcessedMessages)
48 |
49 | def hostedEntitiesPerShardCounter(shardID: String): AtomicLong =
50 | _shardTelemetry.entitiesPerShard.atomicGetOrElseUpdate(shardID, new AtomicLong())
51 |
52 | def processedMessagesPerShardCounter(shardID: String): AtomicLong =
53 | _shardTelemetry.messagesPerShard.atomicGetOrElseUpdate(shardID, new AtomicLong())
54 |
55 | // We should only remove when the ShardRegion actor is terminated.
56 | override def remove(): Unit = {
57 | ShardingInstruments.removeShardTelemetry(system, typeName)
58 | super.remove()
59 | }
60 | }
61 |
62 | object ShardingInstruments {
63 |
64 | /**
65 | * Assist with tracking the number of entities hosted by a Shard and the number of messages processed by each
66 | * Shard. Note that there is a difference in the hosted entities and processed messages at the Region level versus
67 | * at the Shard Level: there is only one Region per type per node, so the number of processed messages is a clear
68 | * indication of how many messages were processed and how many entities are hosted in the region; there can (and
69 | * will be) many Shards on the same node which could generate cardinality issues if we were tracking metrics for
70 | * each individual Shard so, instead, we track the distribution of entities and processed messages across all
71 | * Shards. This behavior can help uncover cases in which Shards are not evenly distributed (both in the messages
72 | * volume and hosted entities aspects) but cannot point out which of the Shards deviates from the common case.
73 | *
74 | * The totals per Shard are tracked locally and sampled in a fixed interval.
75 | */
76 | case class ShardTelemetry (
77 | entitiesPerShard: TrieMap[String, AtomicLong],
78 | messagesPerShard: TrieMap[String, AtomicLong],
79 | schedule: ScheduledFuture[_]
80 | )
81 |
82 | private val _shardTelemetryMap = TrieMap.empty[String, ShardTelemetry]
83 |
84 | private def shardTelemetry(system: String, typeName: String, shardEntities: Histogram, shardMessages: Histogram): ShardTelemetry = {
85 | _shardTelemetryMap.atomicGetOrElseUpdate(shardTelemetryKey(system, typeName), {
86 | val entitiesPerShard = TrieMap.empty[String, AtomicLong]
87 | val messagesPerShard = TrieMap.empty[String, AtomicLong]
88 | val samplingInterval = AkkaRemoteInstrumentation.settings().shardMetricsSampleInterval
89 |
90 | val schedule = Kamon.scheduler().scheduleAtFixedRate(new Runnable {
91 | override def run(): Unit = {
92 | entitiesPerShard.foreach {case (shard, value) => shardEntities.record(value.get())}
93 | messagesPerShard.foreach {case (shard, value) => shardMessages.record(value.getAndSet(0L))}
94 | }
95 | }, samplingInterval.toMillis, samplingInterval.toMillis, TimeUnit.MILLISECONDS)
96 |
97 |
98 | ShardTelemetry(entitiesPerShard, messagesPerShard, schedule)
99 | }, _.schedule.cancel(false): Unit, _ => ())
100 | }
101 |
102 | private def removeShardTelemetry(system: String, typeName: String): Unit =
103 | _shardTelemetryMap.remove(shardTelemetryKey(system, typeName)).foreach(_.schedule.cancel(true))
104 |
105 | private def shardTelemetryKey(system: String, typeName: String): String =
106 | system + ":" + typeName
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/instrumentation/akka-2.6/src/main/scala/kamon/instrumentation/akka/instrumentations/akka_26/remote/internal/ArterySerializationAdvice.scala:
--------------------------------------------------------------------------------
1 | package akka.remote.kamon.instrumentation.akka.instrumentations.akka_26.remote
2 |
3 | import java.nio.ByteBuffer
4 |
5 | import akka.actor.ActorSystem
6 | import akka.remote.artery.{EnvelopeBuffer, OutboundEnvelope}
7 | import akka.serialization.Serialization
8 | import kamon.Kamon
9 | import kamon.context.{BinaryPropagation, Context}
10 | import kamon.instrumentation.akka.AkkaRemoteMetrics
11 | import kamon.instrumentation.context.HasContext
12 | import kanela.agent.libs.net.bytebuddy.asm.Advice
13 |
14 |
15 | /**
16 | * For Artery messages we will always add two sections to the end of each serialized message: the Context and the size
17 | * of the Context. The layout will look something like this:
18 | *
19 | * |------------------ Actual Message ------------------||-- Kamon Context --||-- Context Size (4 bytes) --|
20 | *
21 | * If the Context is empty the Context size will be zero.
22 | */
23 |
24 | class SerializeForArteryAdvice
25 | object SerializeForArteryAdvice {
26 |
27 | @Advice.OnMethodEnter
28 | def enter(): Long = {
29 | System.nanoTime()
30 | }
31 |
32 | @Advice.OnMethodExit
33 | def exit(@Advice.Argument(0) serialization: Serialization, @Advice.Argument(1) envelope: OutboundEnvelope,
34 | @Advice.Argument(3) envelopeBuffer: EnvelopeBuffer, @Advice.Enter startTime: Long): Unit = {
35 |
36 | val instruments = AkkaRemoteMetrics.serializationInstruments(serialization.system.name)
37 | val messageBuffer = envelopeBuffer.byteBuffer
38 | val context = envelope.asInstanceOf[HasContext].context
39 | val positionBeforeContext = messageBuffer.position()
40 |
41 | if(context.nonEmpty()) {
42 | Kamon.defaultBinaryPropagation().write(context, byteBufferWriter(messageBuffer))
43 | }
44 |
45 | instruments.serializationTime.record(System.nanoTime() - startTime)
46 | instruments.outboundMessageSize.record(positionBeforeContext)
47 |
48 | val contextSize = messageBuffer.position() - positionBeforeContext
49 | messageBuffer.putInt(contextSize)
50 | }
51 |
52 | def byteBufferWriter(bb: ByteBuffer): BinaryPropagation.ByteStreamWriter = new BinaryPropagation.ByteStreamWriter {
53 | override def write(bytes: Array[Byte]): Unit =
54 | bb.put(bytes)
55 |
56 | override def write(bytes: Array[Byte], offset: Int, count: Int): Unit =
57 | bb.put(bytes, offset, count)
58 |
59 | override def write(byte: Int): Unit =
60 | bb.put(byte.toByte)
61 | }
62 | }
63 |
64 | class DeserializeForArteryAdvice
65 | object DeserializeForArteryAdvice {
66 |
67 | val LastDeserializedContext = new ThreadLocal[Context]() {
68 | override def initialValue(): Context = null
69 | }
70 |
71 | case class DeserializationInfo(
72 | context: Context,
73 | timeStamp: Long,
74 | messageSize: Long
75 | )
76 |
77 | @Advice.OnMethodEnter
78 | def exit(@Advice.Argument(5) envelopeBuffer: EnvelopeBuffer): DeserializationInfo = {
79 | val startTime = System.nanoTime()
80 | val messageBuffer = envelopeBuffer.byteBuffer
81 | val messageStart = messageBuffer.position()
82 |
83 | messageBuffer.mark()
84 | messageBuffer.position(messageBuffer.limit() - 4)
85 | val contextSize = messageBuffer.getInt()
86 | val contextStart = messageBuffer.limit() - (contextSize + 4)
87 | val messageSize = contextStart - messageStart
88 |
89 | val context = if(contextSize == 0)
90 | Context.Empty
91 | else {
92 | messageBuffer
93 | .position(contextStart)
94 | .limit(contextStart + contextSize)
95 |
96 | Kamon.defaultBinaryPropagation().read(byteBufferReader(messageBuffer))
97 | }
98 |
99 | messageBuffer.reset()
100 | messageBuffer.limit(contextStart)
101 | DeserializationInfo(context, startTime, messageSize)
102 | }
103 |
104 | @Advice.OnMethodExit(onThrowable = classOf[Throwable])
105 | def exit(@Advice.Argument(0) system: ActorSystem, @Advice.Argument(5) envelopeBuffer: EnvelopeBuffer,
106 | @Advice.Enter deserializationInfo: DeserializationInfo, @Advice.Thrown error: Throwable): Unit = {
107 |
108 | if(error == null) {
109 | LastDeserializedContext.set(deserializationInfo.context)
110 |
111 | val instruments = AkkaRemoteMetrics.serializationInstruments(system.name)
112 | instruments.deserializationTime.record(System.nanoTime() - deserializationInfo.timeStamp)
113 | instruments.inboundMessageSize.record(deserializationInfo.messageSize)
114 | }
115 | }
116 |
117 |
118 | def byteBufferReader(bb: ByteBuffer): BinaryPropagation.ByteStreamReader = new BinaryPropagation.ByteStreamReader {
119 | override def available(): Int =
120 | bb.remaining()
121 |
122 | override def read(target: Array[Byte]): Int = {
123 | bb.get(target)
124 | target.length
125 | }
126 |
127 | override def read(target: Array[Byte], offset: Int, count: Int): Int = {
128 | bb.get(target, offset, count)
129 | target.length
130 | }
131 |
132 | override def readAll(): Array[Byte] = {
133 | val array = Array.ofDim[Byte](bb.remaining())
134 | bb.get(array)
135 | array
136 | }
137 | }
138 | }
139 |
140 |
141 | class CaptureContextOnInboundEnvelope
142 | object CaptureContextOnInboundEnvelope {
143 |
144 | @Advice.OnMethodEnter
145 | def enter(@Advice.This inboundEnvelope: Any): Unit = {
146 | val lastContext = DeserializeForArteryAdvice.LastDeserializedContext.get()
147 | if(lastContext != null) {
148 | inboundEnvelope.asInstanceOf[HasContext].setContext(lastContext)
149 | DeserializeForArteryAdvice.LastDeserializedContext.set(null)
150 | }
151 | }
152 |
153 | }
--------------------------------------------------------------------------------
/test/common/src/test/scala/kamon/instrumentation/akka/AutoGroupingSpec.scala:
--------------------------------------------------------------------------------
1 | /* =========================================================================================
2 | * Copyright © 2013-2017 the kamon project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5 | * except in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the
10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
11 | * either express or implied. See the License for the specific language governing permissions
12 | * and limitations under the License.
13 | * =========================================================================================
14 | */
15 |
16 | package kamon.instrumentation.akka
17 |
18 | import akka.actor._
19 | import akka.routing.RoundRobinPool
20 | import akka.testkit.{ImplicitSender, TestKit, TestProbe}
21 | import kamon.instrumentation.akka.ActorMetricsTestActor.Block
22 | import kamon.instrumentation.akka.AkkaMetrics._
23 | import kamon.tag.TagSet
24 | import kamon.testkit.{InstrumentInspection, MetricInspection}
25 | import kamon.util.Filter.Glob
26 | import org.scalactic.TimesOnInt.convertIntToRepeater
27 | import org.scalatest.concurrent.Eventually
28 | import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike}
29 |
30 | import scala.concurrent.duration._
31 |
32 | class AutoGroupingSpec extends TestKit(ActorSystem("AutoGroupingSpec")) with WordSpecLike with MetricInspection.Syntax
33 | with InstrumentInspection.Syntax with Matchers with BeforeAndAfterAll with ImplicitSender with Eventually {
34 |
35 | import AutoGroupingSpec._
36 |
37 | val preExistingGroups = AkkaMetrics.GroupMembers.tagValues("group")
38 |
39 | "auto-grouping" should {
40 | "ignore actors that belong to defined groups or are being tracked" in {
41 | system.actorOf(reproducer(1, 0), "tracked-reproducer") ! "ping"
42 | system.actorOf(dummy(), "tracked-dummy")
43 | expectMsg("pong")
44 |
45 | withoutPreExisting(AkkaMetrics.GroupMembers.tagValues("group")) shouldBe empty
46 | }
47 |
48 | "ignore routers and routees" in {
49 | createTestRouter("tracked-router") ! "ping"
50 | createTestRouter("tracked-explicitly-excluded-router") ! "ping"
51 | expectMsg("pong")
52 | expectMsg("pong")
53 |
54 | withoutPreExisting(AkkaMetrics.GroupMembers.tagValues("group")) shouldBe empty
55 | }
56 |
57 | "automatically create groups for actors that are not being explicitly tracked" in {
58 |
59 | // This will create three levels of actors, all of type "Reproducer" and all should be auto-grouped on their
60 | // own level.
61 | system.actorOf(reproducer(3, 2))
62 | system.actorOf(dummy())
63 |
64 | eventually {
65 | withoutPreExisting(AkkaMetrics.GroupMembers.tagValues("group")) should contain allOf (
66 | "AutoGroupingSpec/user/Dummy",
67 | "AutoGroupingSpec/user/Reproducer",
68 | "AutoGroupingSpec/user/Reproducer/Reproducer",
69 | "AutoGroupingSpec/user/Reproducer/Reproducer/Reproducer"
70 | )
71 | }
72 |
73 | val topGroup = AkkaMetrics.forGroup("AutoGroupingSpec/user/Reproducer", system.name)
74 | val secondLevelGroup = AkkaMetrics.forGroup("AutoGroupingSpec/user/Reproducer/Reproducer", system.name)
75 | val thirdLevelGroup = AkkaMetrics.forGroup("AutoGroupingSpec/user/Reproducer/Reproducer/Reproducer", system.name)
76 | val dummyGroup = AkkaMetrics.forGroup("AutoGroupingSpec/user/Dummy", system.name)
77 |
78 | eventually {
79 | topGroup.members.distribution(resetState = false).max shouldBe 1
80 | secondLevelGroup.members.distribution(resetState = false).max shouldBe 2
81 | thirdLevelGroup.members.distribution(resetState = false).max shouldBe 8
82 | dummyGroup.members.distribution(resetState = false).max shouldBe 1
83 | }
84 | }
85 | }
86 |
87 | def withoutPreExisting(values: Seq[String]): Seq[String] =
88 | values.filter(v => preExistingGroups.indexOf(v) < 0)
89 |
90 | override implicit def patienceConfig: PatienceConfig =
91 | PatienceConfig(timeout = 5 seconds, interval = 5 milliseconds)
92 |
93 | override protected def afterAll(): Unit =
94 | shutdown()
95 |
96 | def createTestRouter(routerName: String): ActorRef = {
97 | val router = system.actorOf(RoundRobinPool(5).props(reproducer(1, 0)), routerName)
98 | val initialiseListener = TestProbe()
99 |
100 | // Ensure that the router has been created before returning.
101 | router.tell("ping", initialiseListener.ref)
102 | initialiseListener.expectMsg("pong")
103 |
104 | router
105 | }
106 | }
107 |
108 | object AutoGroupingSpec {
109 |
110 | class Reproducer(pendingDepth: Int, childCount: Int) extends Actor {
111 |
112 | override def receive: Receive = {
113 | case "ping" => sender() ! "pong"
114 | case other => context.children.foreach(_.forward(other))
115 | }
116 |
117 | override def preStart(): Unit = {
118 | super.preStart()
119 |
120 | if(pendingDepth >= 0) {
121 | childCount.times {
122 | context.actorOf(reproducer(pendingDepth - 1, childCount * 2)) ! "ping"
123 | }
124 | }
125 | }
126 | }
127 |
128 | class Dummy extends Actor {
129 | override def receive: Receive = {
130 | case _ =>
131 | }
132 | }
133 |
134 | def reproducer(depth: Int, childCount: Int): Props =
135 | Props(new Reproducer(depth - 1, childCount))
136 |
137 | def dummy(): Props =
138 | Props[Dummy]
139 | }
140 |
141 |
--------------------------------------------------------------------------------
/test/common/src/test/scala/kamon/instrumentation/akka/ActorCellInstrumentationSpec.scala:
--------------------------------------------------------------------------------
1 | /* ===================================================
2 | * Copyright © 2013 the kamon project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | * ========================================================== */
16 | package kamon.instrumentation.akka
17 |
18 | import akka.actor.{Actor, ActorRef, ActorSystem, PoisonPill, Props}
19 | import akka.pattern.{ask, pipe}
20 | import akka.routing._
21 | import akka.testkit.{ImplicitSender, TestKit}
22 | import akka.util.Timeout
23 | import kamon.Kamon
24 | import kamon.testkit.MetricInspection
25 | import org.scalatest.concurrent.Eventually
26 | import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike}
27 | import kamon.tag.Lookups._
28 |
29 | import scala.collection.mutable.ListBuffer
30 | import scala.concurrent.duration._
31 |
32 |
33 | class ActorCellInstrumentationSpec extends TestKit(ActorSystem("ActorCellInstrumentationSpec")) with WordSpecLike
34 | with BeforeAndAfterAll with ImplicitSender with Eventually with MetricInspection.Syntax with Matchers {
35 | implicit lazy val executionContext = system.dispatcher
36 | import ContextTesting._
37 |
38 | "the message passing instrumentation" should {
39 | "capture and propagate the current context when using bang" in new EchoActorFixture {
40 | Kamon.runWithContext(testContext("propagate-with-bang")) {
41 | contextEchoActor ! "test"
42 | }
43 |
44 | expectMsg("propagate-with-bang")
45 | }
46 |
47 | "capture and propagate the current context for messages sent when the target actor might be a repointable ref" in {
48 | for (_ <- 1 to 100) {
49 | val ta = system.actorOf(Props[ContextStringEcho])
50 | Kamon.runWithContext(testContext("propagate-with-tell")) {
51 | ta.tell("test", testActor)
52 | }
53 |
54 | expectMsg("propagate-with-tell")
55 | system.stop(ta)
56 | }
57 | }
58 |
59 | "propagate the current context when using the ask pattern" in new EchoActorFixture {
60 | implicit val timeout = Timeout(1 seconds)
61 | Kamon.runWithContext(testContext("propagate-with-ask")) {
62 | // The pipe pattern use Futures internally, so FutureTracing test should cover the underpinnings of it.
63 | (contextEchoActor ? "test") pipeTo (testActor)
64 | }
65 |
66 | expectMsg("propagate-with-ask")
67 | }
68 |
69 |
70 | "propagate the current context to actors behind a simple router" in new EchoSimpleRouterFixture {
71 | Kamon.runWithContext(testContext("propagate-with-router")) {
72 | router.route("test", testActor)
73 | }
74 |
75 | expectMsg("propagate-with-router")
76 | }
77 |
78 | "propagate the current context to actors behind a pool router" in new EchoPoolRouterFixture {
79 | Kamon.runWithContext(testContext("propagate-with-pool")) {
80 | pool ! "test"
81 | }
82 |
83 | expectMsg("propagate-with-pool")
84 | }
85 |
86 | "propagate the current context to actors behind a group router" in new EchoGroupRouterFixture {
87 | Kamon.runWithContext(testContext("propagate-with-group")) {
88 | group ! "test"
89 | }
90 |
91 | expectMsg("propagate-with-group")
92 | }
93 |
94 | "cleanup the metric recorders when a RepointableActorRef is killed early" in {
95 | def actorPathTag(ref: ActorRef): String = system.name + "/" + ref.path.elements.mkString("/")
96 | val trackedActors = new ListBuffer[String]
97 |
98 | for(j <- 1 to 10) {
99 | for (i <- 1 to 1000) {
100 | val a = system.actorOf(Props[ContextStringEcho], s"repointable-$j-$i")
101 | a ! PoisonPill
102 | trackedActors.append(actorPathTag(a))
103 | }
104 |
105 | eventually(timeout(1 second)) {
106 | val trackedActors = AkkaMetrics.ActorProcessingTime.tagValues("path")
107 | for(p <- trackedActors) {
108 | trackedActors.find(_ == p) shouldBe empty
109 | }
110 | }
111 |
112 | trackedActors.clear()
113 | }
114 | }
115 | }
116 |
117 | override protected def afterAll(): Unit = shutdown()
118 |
119 | trait EchoActorFixture {
120 | val contextEchoActor = system.actorOf(Props[ContextStringEcho])
121 | }
122 |
123 | trait EchoSimpleRouterFixture {
124 | val router = {
125 | val routees = Vector.fill(5) {
126 | val r = system.actorOf(Props[ContextStringEcho])
127 | ActorRefRoutee(r)
128 | }
129 | Router(RoundRobinRoutingLogic(), routees)
130 | }
131 | }
132 |
133 | trait EchoPoolRouterFixture {
134 | val pool = system.actorOf(RoundRobinPool(nrOfInstances = 5).props(Props[ContextStringEcho]), "pool-router")
135 | }
136 |
137 | trait EchoGroupRouterFixture {
138 | val routees = Vector.fill(5) {
139 | system.actorOf(Props[ContextStringEcho])
140 | }
141 |
142 | val group = system.actorOf(RoundRobinGroup(routees.map(_.path.toStringWithoutAddress)).props(), "group-router")
143 | }
144 | }
145 |
146 | class ContextStringEcho extends Actor {
147 | import ContextTesting._
148 |
149 | def receive = {
150 | case _: String =>
151 | sender ! Kamon.currentContext().getTag(plain(TestKey))
152 | }
153 | }
154 |
155 |
--------------------------------------------------------------------------------
/instrumentation/common/src/main/scala/kamon/instrumentation/akka/AkkaInstrumentation.scala:
--------------------------------------------------------------------------------
1 | package kamon.instrumentation.akka
2 |
3 | import com.typesafe.config.{Config, ConfigFactory}
4 | import kamon.Kamon
5 | import kamon.instrumentation.akka.AkkaInstrumentation.AskPatternTimeoutWarningSetting.{Heavyweight, Lightweight, Off}
6 | import kamon.util.Filter
7 |
8 | import scala.collection.JavaConverters.{asScalaBufferConverter, asScalaSetConverter}
9 |
10 | object AkkaInstrumentation {
11 |
12 | val TrackActorFilterName = "kamon.instrumentation.akka.filters.actors.track"
13 | val TraceActorFilterName = "kamon.instrumentation.akka.filters.actors.trace"
14 | val StartTraceActorFilterName = "kamon.instrumentation.akka.filters.actors.start-trace"
15 | val TrackAutoGroupFilterName = "kamon.instrumentation.akka.filters.groups.auto-grouping"
16 | val TrackRouterFilterName = "kamon.instrumentation.akka.filters.routers"
17 | val TrackDispatcherFilterName = "kamon.instrumentation.akka.filters.dispatchers"
18 |
19 | @volatile private var _settings = Settings.from(Kamon.config())
20 | @volatile private var _actorGroups = Map.empty[String, Filter]
21 | @volatile private var _configProvidedActorGroups = Map.empty[String, Filter]
22 | @volatile private var _codeProvidedActorGroups = Map.empty[String, Filter]
23 |
24 | loadConfiguration(Kamon.config())
25 | Kamon.onReconfigure(loadConfiguration(_))
26 |
27 | /**
28 | * Returns the current Akka Instrumentation settings.
29 | */
30 | def settings(): AkkaInstrumentation.Settings =
31 | _settings
32 |
33 | /**
34 | * Returns all Actor Group names that should contain an actor with the provided path.
35 | */
36 | def matchingActorGroups(path: String): Seq[String] = {
37 | _actorGroups.filter { case (_, v) => v.accept(path) }.keys.toSeq
38 | }
39 |
40 | /**
41 | * Creates a new Actor Group definition. Take into account that Actors are added to Actor Groups during their
42 | * initialization process only, which means that a newly defined Actor Group will only include matching actors
43 | * created after the definition succeeded.
44 | *
45 | * Returns true if the definition was successful and false if a group with the defined name is already available.
46 | */
47 | def defineActorGroup(groupName: String, filter: Filter): Boolean = synchronized {
48 | if(_codeProvidedActorGroups.get(groupName).isEmpty) {
49 | _codeProvidedActorGroups = _codeProvidedActorGroups + (groupName -> filter)
50 | _actorGroups = _codeProvidedActorGroups ++ _configProvidedActorGroups
51 | true
52 | } else false
53 | }
54 |
55 | /**
56 | * Removes a programmatically created Actor Group definition. This method can only remove definitions that were
57 | * created via the "defineActorGroup" method.
58 | */
59 | def removeActorGroup(groupName: String): Unit = synchronized {
60 | _codeProvidedActorGroups = _codeProvidedActorGroups - groupName
61 | _actorGroups = _codeProvidedActorGroups ++ _configProvidedActorGroups
62 | }
63 |
64 | private def loadConfiguration(config: Config): Unit = synchronized {
65 | val akkaConfig = config.getConfig("kamon.instrumentation.akka")
66 | val groupsConfig = akkaConfig.getConfig("filters.groups")
67 |
68 | _configProvidedActorGroups = groupsConfig.root.entrySet().asScala
69 | .filter(_.getKey != "auto-grouping")
70 | .map(entry => {
71 | val groupName = entry.getKey
72 | groupName -> Filter.from(groupsConfig.getConfig(groupName))
73 | }).toMap
74 |
75 | _actorGroups = _codeProvidedActorGroups ++ _configProvidedActorGroups
76 | _settings = Settings.from(config)
77 | }
78 |
79 | /**
80 | * Akka Instrumentation settings
81 | */
82 | case class Settings (
83 | askPatternWarning: AskPatternTimeoutWarningSetting,
84 | autoGrouping: Boolean,
85 | allowDoomsdayWildcards: Boolean,
86 | safeActorTrackFilter: Filter,
87 | safeActorStartTraceFilter: Filter
88 | )
89 |
90 | object Settings {
91 |
92 | def from(config: Config): Settings = {
93 | val akkaConfig = config.getConfig("kamon.instrumentation.akka")
94 | val allowDoomsdayWildcards = akkaConfig.getBoolean("filters.actors.doomsday-wildcard")
95 |
96 | val askPatternWarning = akkaConfig.getString("ask-pattern-timeout-warning") match {
97 | case "off" => Off
98 | case "lightweight" => Lightweight
99 | case "heavyweight" => Heavyweight
100 | case other => sys.error(s"Unrecognized option [$other] for the kamon.akka.ask-pattern-timeout-warning config.")
101 | }
102 |
103 | AkkaInstrumentation.Settings(
104 | askPatternWarning,
105 | akkaConfig.getBoolean("auto-grouping"),
106 | allowDoomsdayWildcards,
107 | safeFilter(config.getConfig(TrackActorFilterName), allowDoomsdayWildcards),
108 | safeFilter(config.getConfig(StartTraceActorFilterName), allowDoomsdayWildcards)
109 | )
110 | }
111 |
112 | private def safeFilter(config: Config, allowDoomsday: Boolean): Filter = {
113 | val includes = config.getStringList("includes").asScala
114 | if(!allowDoomsday && includes.contains("**")) {
115 | val newIncludes = "includes = " + includes.filter(_ == "**").map(s => s""""$s"""").mkString("[ ", ", ", " ]")
116 | val safeFilterConfig = ConfigFactory.parseString(newIncludes).withFallback(config)
117 |
118 | Filter.from(safeFilterConfig)
119 |
120 | } else Filter.from(config)
121 | }
122 | }
123 |
124 | sealed trait AskPatternTimeoutWarningSetting
125 | object AskPatternTimeoutWarningSetting {
126 | case object Off extends AskPatternTimeoutWarningSetting
127 | case object Lightweight extends AskPatternTimeoutWarningSetting
128 | case object Heavyweight extends AskPatternTimeoutWarningSetting
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/test/common/src/test/scala/kamon/instrumentation/akka/DispatcherMetricsSpec.scala:
--------------------------------------------------------------------------------
1 | /* =========================================================================================
2 | * Copyright © 2013-2015 the kamon project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5 | * except in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the
10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
11 | * either express or implied. See the License for the specific language governing permissions
12 | * and limitations under the License.
13 | * =========================================================================================
14 | */
15 |
16 | package kamon.instrumentation.akka
17 |
18 | import java.util.concurrent.Executors
19 |
20 | import akka.actor.{ActorSystem, Props}
21 | import akka.dispatch.MessageDispatcher
22 | import akka.routing.BalancingPool
23 | import akka.testkit.{ImplicitSender, TestKit, TestProbe}
24 | import kamon.tag.Lookups.plain
25 | import kamon.instrumentation.executor.ExecutorMetrics
26 | import kamon.testkit.MetricInspection
27 | import org.scalatest.concurrent.Eventually
28 | import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike}
29 | import RouterMetricsTestActor._
30 | import akka.Version
31 |
32 | import scala.concurrent.{ExecutionContext, Future}
33 |
34 | class DispatcherMetricsSpec extends TestKit(ActorSystem("DispatcherMetricsSpec")) with WordSpecLike with MetricInspection.Syntax with Matchers
35 | with BeforeAndAfterAll with ImplicitSender with Eventually {
36 |
37 | "the Kamon dispatcher metrics" should {
38 |
39 | val trackedDispatchers = Seq(
40 | "akka.actor.default-dispatcher",
41 | "tracked-pinned-dispatcher",
42 | "tracked-fjp",
43 | "tracked-tpe"
44 | ) ++ (if(Version.current.startsWith("2.6")) Seq("akka.actor.internal-dispatcher") else Seq.empty)
45 |
46 | val excluded = "explicitly-excluded"
47 | val allDispatchers = trackedDispatchers :+ excluded
48 | val builtInDispatchers = Seq("akka.actor.default-dispatcher")++ (if(Version.current.startsWith("2.6")) Seq("akka.actor.internal-dispatcher") else Seq.empty)
49 |
50 |
51 | "track dispatchers configured in the akka.dispatcher filter" in {
52 | allDispatchers.foreach(id => forceInit(system.dispatchers.lookup(id)))
53 |
54 | val threads = ExecutorMetrics.ThreadsActive.tagValues("name")
55 | val queues = ExecutorMetrics.QueueSize.tagValues("name")
56 | val tasks = ExecutorMetrics.TasksCompleted.tagValues("name")
57 |
58 | trackedDispatchers.forall { dispatcher =>
59 | threads.contains(dispatcher) &&
60 | queues.contains(dispatcher) &&
61 | tasks.contains(dispatcher)
62 | } should be (true)
63 |
64 | Seq(threads, queues, tasks).flatten should not contain excluded
65 | }
66 |
67 | "include the actor system name in the executor tags" in {
68 | val instrumentExecutorsWithSystem = ExecutorMetrics.ThreadsActive.instruments().keys
69 | .filter(_.get(plain("akka.system")) == system.name)
70 | .map(_.get(plain("name")))
71 |
72 | instrumentExecutorsWithSystem should contain only(trackedDispatchers: _*)
73 | }
74 |
75 |
76 | "clean up the metrics recorders after a dispatcher is shutdown" in {
77 | ExecutorMetrics.Parallelism.tagValues("name") should contain("tracked-fjp")
78 | shutdownDispatcher(system.dispatchers.lookup("tracked-fjp"))
79 | Thread.sleep(2000)
80 | ExecutorMetrics.Parallelism.tagValues("name") shouldNot contain("tracked-fjp")
81 | }
82 |
83 | "play nicely when dispatchers are looked up from a BalancingPool router" in {
84 | val balancingPoolRouter = system.actorOf(BalancingPool(5).props(Props[RouterMetricsTestActor]), "test-balancing-pool")
85 | balancingPoolRouter ! Ping
86 | expectMsg(Pong)
87 |
88 | ExecutorMetrics.Parallelism.tagValues("name") should contain("BalancingPool-/test-balancing-pool")
89 | }
90 |
91 | "pick up default execution contexts provided when creating an actor system" in {
92 | val dec = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(8))
93 | val system = ActorSystem(name = "with-default-ec", defaultExecutionContext = Some(dec))
94 |
95 | val instrumentExecutorsWithSystem = ExecutorMetrics.ThreadsActive.instruments().keys
96 | .filter(_.get(plain("akka.system")) == system.name)
97 | .map(_.get(plain("name")))
98 |
99 | instrumentExecutorsWithSystem should contain only(builtInDispatchers: _*)
100 | }
101 |
102 | "pick up default execution contexts provided when creating an actor system when the type is unknown" in {
103 | val dec = new WrappingExecutionContext(ExecutionContext.fromExecutor(Executors.newFixedThreadPool(8)))
104 | val system = ActorSystem(name = "with-default-ec", defaultExecutionContext = Some(dec))
105 |
106 | val instrumentExecutorsWithSystem = ExecutorMetrics.ThreadsActive.instruments().keys
107 | .filter(_.get(plain("akka.system")) == system.name)
108 | .map(_.get(plain("name")))
109 |
110 | instrumentExecutorsWithSystem should contain only(builtInDispatchers: _*)
111 | }
112 | }
113 |
114 |
115 | def forceInit(dispatcher: MessageDispatcher): MessageDispatcher = {
116 | val listener = TestProbe()
117 | Future {
118 | listener.ref ! "init done"
119 | }(dispatcher)
120 | listener.expectMsg("init done")
121 |
122 | dispatcher
123 | }
124 |
125 | def shutdownDispatcher(dispatcher: MessageDispatcher): Unit = {
126 | val shutdownMethod = dispatcher.getClass.getDeclaredMethod("shutdown")
127 | shutdownMethod.setAccessible(true)
128 | shutdownMethod.invoke(dispatcher)
129 | }
130 |
131 | override protected def afterAll(): Unit = system.terminate()
132 |
133 | class WrappingExecutionContext(ec: ExecutionContext) extends ExecutionContext {
134 | override def execute(runnable: Runnable): Unit = ec.execute(runnable)
135 | override def reportFailure(cause: Throwable): Unit = ec.reportFailure(cause)
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/instrumentation/akka-2.5/src/main/scala/kamon/instrumentation/akka/instrumentations/akka_25/remote/internal/ArterySerializationAdvice.scala:
--------------------------------------------------------------------------------
1 | package akka.remote.kamon.instrumentation.akka.instrumentations.akka_25.remote
2 |
3 | import java.nio.ByteBuffer
4 |
5 | import akka.actor.ActorSystem
6 | import akka.dispatch.sysmsg.SystemMessage
7 | import akka.remote.artery.{EnvelopeBuffer, InboundEnvelope, OutboundEnvelope}
8 | import akka.serialization.Serialization
9 | import kamon.Kamon
10 | import kamon.context.{BinaryPropagation, Context, Storage}
11 | import kamon.instrumentation.akka.AkkaRemoteMetrics
12 | import kamon.instrumentation.context.HasContext
13 | import kanela.agent.libs.net.bytebuddy.asm.Advice
14 |
15 |
16 | /**
17 | * For Artery messages we will always add two sections to the end of each serialized message: the Context and the size
18 | * of the Context. The layout will look something like this:
19 | *
20 | * |------------------ Actual Message ------------------||-- Kamon Context --||-- Context Size (4 bytes) --|
21 | *
22 | * If the Context is empty the Context size will be zero.
23 | */
24 |
25 | class SerializeForArteryAdvice
26 | object SerializeForArteryAdvice {
27 |
28 | @Advice.OnMethodEnter
29 | def enter(): Long = {
30 | System.nanoTime()
31 | }
32 |
33 | @Advice.OnMethodExit
34 | def exit(@Advice.Argument(0) serialization: Serialization, @Advice.Argument(1) envelope: OutboundEnvelope,
35 | @Advice.Argument(3) envelopeBuffer: EnvelopeBuffer, @Advice.Enter startTime: Long): Unit = {
36 |
37 | val instruments = AkkaRemoteMetrics.serializationInstruments(serialization.system.name)
38 | val messageBuffer = envelopeBuffer.byteBuffer
39 | val context = envelope.asInstanceOf[HasContext].context
40 | val positionBeforeContext = messageBuffer.position()
41 |
42 | if(context.nonEmpty()) {
43 | Kamon.defaultBinaryPropagation().write(context, byteBufferWriter(messageBuffer))
44 | }
45 |
46 | instruments.serializationTime.record(System.nanoTime() - startTime)
47 | instruments.outboundMessageSize.record(positionBeforeContext)
48 |
49 | val contextSize = messageBuffer.position() - positionBeforeContext
50 | messageBuffer.putInt(contextSize)
51 | }
52 |
53 | def byteBufferWriter(bb: ByteBuffer): BinaryPropagation.ByteStreamWriter = new BinaryPropagation.ByteStreamWriter {
54 | override def write(bytes: Array[Byte]): Unit =
55 | bb.put(bytes)
56 |
57 | override def write(bytes: Array[Byte], offset: Int, count: Int): Unit =
58 | bb.put(bytes, offset, count)
59 |
60 | override def write(byte: Int): Unit =
61 | bb.put(byte.toByte)
62 | }
63 | }
64 |
65 | class DeserializeForArteryAdvice
66 | object DeserializeForArteryAdvice {
67 |
68 | val LastDeserializedContext = new ThreadLocal[Context]() {
69 | override def initialValue(): Context = null
70 | }
71 |
72 | case class DeserializationInfo(
73 | context: Context,
74 | timeStamp: Long,
75 | messageSize: Long
76 | )
77 |
78 | @Advice.OnMethodEnter
79 | def exit(@Advice.Argument(5) envelopeBuffer: EnvelopeBuffer): DeserializationInfo = {
80 | val startTime = System.nanoTime()
81 | val messageBuffer = envelopeBuffer.byteBuffer
82 | val messageStart = messageBuffer.position()
83 |
84 | messageBuffer.mark()
85 | messageBuffer.position(messageBuffer.limit() - 4)
86 | val contextSize = messageBuffer.getInt()
87 | val contextStart = messageBuffer.limit() - (contextSize + 4)
88 | val messageSize = contextStart - messageStart
89 |
90 | val context = if(contextSize == 0)
91 | Context.Empty
92 | else {
93 | messageBuffer
94 | .position(contextStart)
95 | .limit(contextStart + contextSize)
96 |
97 | Kamon.defaultBinaryPropagation().read(byteBufferReader(messageBuffer))
98 | }
99 |
100 | messageBuffer.reset()
101 | messageBuffer.limit(contextStart)
102 | DeserializationInfo(context, startTime, messageSize)
103 | }
104 |
105 | @Advice.OnMethodExit(onThrowable = classOf[Throwable])
106 | def exit(@Advice.Argument(0) system: ActorSystem, @Advice.Argument(5) envelopeBuffer: EnvelopeBuffer,
107 | @Advice.Enter deserializationInfo: DeserializationInfo, @Advice.Thrown error: Throwable): Unit = {
108 |
109 | if(error == null) {
110 | LastDeserializedContext.set(deserializationInfo.context)
111 |
112 | val instruments = AkkaRemoteMetrics.serializationInstruments(system.name)
113 | instruments.deserializationTime.record(System.nanoTime() - deserializationInfo.timeStamp)
114 | instruments.inboundMessageSize.record(deserializationInfo.messageSize)
115 | }
116 | }
117 |
118 |
119 | def byteBufferReader(bb: ByteBuffer): BinaryPropagation.ByteStreamReader = new BinaryPropagation.ByteStreamReader {
120 | override def available(): Int =
121 | bb.remaining()
122 |
123 | override def read(target: Array[Byte]): Int = {
124 | bb.get(target)
125 | target.length
126 | }
127 |
128 | override def read(target: Array[Byte], offset: Int, count: Int): Int = {
129 | bb.get(target, offset, count)
130 | target.length
131 | }
132 |
133 | override def readAll(): Array[Byte] = {
134 | val array = Array.ofDim[Byte](bb.remaining())
135 | bb.get(array)
136 | array
137 | }
138 | }
139 | }
140 |
141 | class CaptureContextOnInboundEnvelope
142 | object CaptureContextOnInboundEnvelope {
143 |
144 | @Advice.OnMethodEnter
145 | def enter(@Advice.This inboundEnvelope: Any): Unit = {
146 | val lastContext = DeserializeForArteryAdvice.LastDeserializedContext.get()
147 | if(lastContext != null) {
148 | inboundEnvelope.asInstanceOf[HasContext].setContext(lastContext)
149 | DeserializeForArteryAdvice.LastDeserializedContext.set(null)
150 | }
151 | }
152 |
153 | }
154 |
155 | class ArteryMessageDispatcherAdvice
156 | object ArteryMessageDispatcherAdvice {
157 |
158 | @Advice.OnMethodEnter
159 | def enter(@Advice.Argument(0) envelope: InboundEnvelope): Storage.Scope = {
160 | val context = envelope.asInstanceOf[HasContext].context
161 | if(envelope.message.isInstanceOf[SystemMessage]) {
162 | envelope.message.asInstanceOf[HasContext].setContext(context)
163 | }
164 |
165 | Kamon.storeContext(envelope.asInstanceOf[HasContext].context)
166 | }
167 |
168 | @Advice.OnMethodExit
169 | def exit(@Advice.Enter scope: Storage.Scope): Unit =
170 | scope.close()
171 | }
--------------------------------------------------------------------------------
/test/common/src/test/scala/kamon/instrumentation/akka/sharding/ShardingInstrumentationSpec.scala:
--------------------------------------------------------------------------------
1 | package akka.kamon.instrumentation.akka.sharding
2 |
3 | import akka.actor._
4 | import akka.cluster.sharding.ShardCoordinator.Internal.{HandOff, ShardStopped}
5 | import akka.cluster.sharding.ShardCoordinator.ShardAllocationStrategy
6 | import akka.cluster.sharding.ShardRegion.{GracefulShutdown, ShardId}
7 | import akka.cluster.sharding.{ClusterSharding, ClusterShardingSettings, ShardRegion}
8 | import akka.testkit.TestActor.Watch
9 | import akka.testkit.{ImplicitSender, TestKitBase}
10 | import com.typesafe.config.ConfigFactory
11 | import kamon.instrumentation.akka.AkkaClusterShardingMetrics._
12 | import kamon.tag.TagSet
13 | import kamon.testkit.{InstrumentInspection, MetricInspection}
14 | import org.scalactic.TimesOnInt.convertIntToRepeater
15 | import org.scalatest.concurrent.Eventually
16 | import org.scalatest.{Matchers, WordSpecLike}
17 |
18 | import scala.collection.immutable
19 | import scala.concurrent.Future
20 | import scala.util.Random
21 | import org.scalatest.time._
22 |
23 | case class TestMessage(shard: String, entity: String)
24 |
25 | class ShardingInstrumentationSpec
26 | extends TestKitBase
27 | with WordSpecLike
28 | with Matchers
29 | with ImplicitSender
30 | with MetricInspection.Syntax
31 | with InstrumentInspection.Syntax
32 | with Eventually {
33 |
34 | lazy val system: ActorSystem = {
35 | ActorSystem(
36 | "sharding",
37 | ConfigFactory
38 | .parseString("""
39 | |akka {
40 | | actor.provider = "cluster"
41 | | remote {
42 | | enabled-transports = ["akka.remote.netty.tcp"]
43 | | netty.tcp {
44 | | hostname = "127.0.0.1"
45 | | port = 2551
46 | | }
47 | | }
48 | | cluster {
49 | | seed-nodes = ["akka.tcp://sharding@127.0.0.1:2551"]
50 | | log-info = on
51 | | cluster.jmx.multi-mbeans-in-same-jvm = on
52 | | }
53 | |}
54 | """.stripMargin)
55 | .withFallback(ConfigFactory.load())
56 | )
57 | }
58 |
59 | val entityIdExtractor: ShardRegion.ExtractEntityId = { case msg @ TestMessage(_, entity) => (entity, msg) }
60 | val shardIdExtractor: ShardRegion.ExtractShardId = { case msg @ TestMessage(shard, _) => shard }
61 |
62 | val StaticAllocationStrategy = new ShardAllocationStrategy {
63 | override def allocateShard(
64 | requester: ActorRef,
65 | shardId: ShardId,
66 | currentShardAllocations: Map[ActorRef, immutable.IndexedSeq[ShardId]])
67 | : Future[ActorRef] = {
68 | Future.successful(requester)
69 | }
70 |
71 | override def rebalance(
72 | currentShardAllocations: Map[ActorRef, immutable.IndexedSeq[ShardId]],
73 | rebalanceInProgress: Set[ShardId]): Future[Set[ShardId]] = {
74 | Future.successful(Set.empty)
75 | }
76 | }
77 |
78 | def registerTypes(shardedType: String, props: Props, system: ActorSystem, allocationStrategy: ShardAllocationStrategy): ActorRef =
79 | ClusterSharding(system).start(
80 | typeName = shardedType,
81 | entityProps = props,
82 | settings = ClusterShardingSettings(system),
83 | extractEntityId = entityIdExtractor,
84 | extractShardId = shardIdExtractor,
85 | allocationStrategy = allocationStrategy,
86 | handOffStopMessage = PoisonPill
87 | )
88 |
89 | class ShardedTypeContext {
90 | val shardType = s"TestType-${Random.nextLong()}"
91 | val region = registerTypes(shardType, TestActor.props(testActor), system, StaticAllocationStrategy)
92 | val shardTags = TagSet.builder()
93 | .add("type", shardType)
94 | .add("system", system.name)
95 | .build()
96 | }
97 |
98 | "the Cluster sharding instrumentation" should {
99 | "track shards, entities and messages" in new ShardedTypeContext {
100 | region ! TestMessage("s1", "e1")
101 | region ! TestMessage("s1", "e2")
102 | region ! TestMessage("s2", "e3")
103 |
104 | 3 times {
105 | expectMsg("OK")
106 | }
107 |
108 | RegionProcessedMessages.withTags(shardTags).value() shouldBe 3L
109 |
110 | eventually(timeout(Span(2, Seconds))) {
111 | RegionHostedShards.withTags(shardTags).distribution().max shouldBe 2L
112 | RegionHostedEntities.withTags(shardTags).distribution().max shouldBe 3L
113 | }
114 |
115 | eventually(timeout(Span(2, Seconds))) {
116 | ShardProcessedMessages.withTags(shardTags).distribution(resetState = false).sum shouldBe 3L
117 | ShardHostedEntities.withTags(shardTags).distribution(resetState = false).max shouldBe 2L
118 | }
119 | }
120 |
121 | "clean metrics on handoff" in new ShardedTypeContext {
122 | region ! TestMessage("s1", "e1")
123 | expectMsg("OK")
124 |
125 | eventually(timeout(Span(2, Seconds))) {
126 | RegionHostedShards.withTags(shardTags).distribution().max shouldBe 1L
127 | RegionHostedEntities.withTags(shardTags).distribution().max shouldBe 1L
128 | }
129 |
130 | region ! HandOff("s1")
131 | expectMsg(ShardStopped("s1"))
132 |
133 | eventually(timeout(Span(10, Seconds))) {
134 | RegionHostedShards.withTags(shardTags).distribution().max shouldBe 0L
135 | RegionHostedEntities.withTags(shardTags).distribution().max shouldBe 0L
136 | }
137 | }
138 |
139 | "clean metrics on shutdown" in new ShardedTypeContext {
140 | region ! TestMessage("s1", "e1")
141 | expectMsg("OK")
142 |
143 | RegionHostedShards.tagValues("type") should contain(shardType)
144 | RegionHostedEntities.tagValues("type") should contain(shardType)
145 | RegionProcessedMessages.tagValues("type") should contain(shardType)
146 |
147 | testActor ! Watch(region)
148 | region ! GracefulShutdown
149 | expectTerminated(region)
150 |
151 | RegionHostedShards.tagValues("type") should not contain(shardType)
152 | RegionHostedEntities.tagValues("type") should not contain(shardType)
153 | RegionProcessedMessages.tagValues("type") should not contain(shardType)
154 | }
155 | }
156 |
157 | }
158 |
159 | object TestActor {
160 |
161 | def props(testActor: ActorRef) =
162 | Props(classOf[TestActor], testActor)
163 | }
164 |
165 | class TestActor(testActor: ActorRef) extends Actor {
166 |
167 | override def receive: Actor.Receive = {
168 | case _ => testActor ! "OK"
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/test/akka-2.6/src/test/scala/kamon/instrumentation/akka/remote/ArteryTcpRemotingInstrumentationSpec.scala:
--------------------------------------------------------------------------------
1 | package kamon.instrumentation.akka.remote
2 |
3 | import akka.actor._
4 | import akka.pattern.{ask, pipe}
5 | import akka.routing.RoundRobinGroup
6 | import akka.testkit.{ImplicitSender, TestKitBase}
7 | import akka.util.Timeout
8 | import com.typesafe.config.ConfigFactory
9 | import kamon.Kamon
10 | import kamon.context.Context
11 | import kamon.instrumentation.akka.AkkaRemoteMetrics.{DeserializationTime, SerializationTime}
12 | import kamon.instrumentation.akka.{AkkaRemoteMetrics, ContextEchoActor}
13 | import kamon.tag.TagSet
14 | import kamon.testkit.{InstrumentInspection, MetricInspection}
15 | import org.scalatest.Inspectors._
16 | import org.scalatest.{Matchers, WordSpecLike}
17 |
18 | import scala.concurrent.duration._
19 |
20 | class ArteryTcpRemotingInstrumentationSpec extends TestKitBase with WordSpecLike with Matchers with ImplicitSender
21 | with MetricInspection.Syntax with InstrumentInspection.Syntax {
22 |
23 | implicit lazy val system: ActorSystem = {
24 | ActorSystem("remoting-spec-local-system", ConfigFactory.parseString(
25 | """
26 | |akka {
27 | | actor {
28 | | provider = cluster
29 | | }
30 | |
31 | | remote {
32 | | artery {
33 | | transport = tcp
34 | | canonical.hostname = "127.0.0.1"
35 | | canonical.port = 2554
36 | | }
37 | | }
38 | |}
39 | """.stripMargin))
40 | }
41 |
42 | val remoteSystem: ActorSystem = ActorSystem("remoting-spec-remote-system", ConfigFactory.parseString(
43 | """
44 | |akka {
45 | | actor {
46 | | provider = cluster
47 | | }
48 | |
49 | | remote {
50 | | artery {
51 | | transport = tcp
52 | | canonical.hostname = "127.0.0.1"
53 | | canonical.port = 2555
54 | | }
55 | | }
56 | |}
57 | """.stripMargin))
58 |
59 | val RemoteSystemAddress = AddressFromURIString("akka://remoting-spec-remote-system@127.0.0.1:2555")
60 |
61 | def contextWithNameTag(name: String): Context =
62 | Context.Empty.withTag(
63 | ContextEchoActor.EchoTag,
64 | name
65 | )
66 |
67 | "The Akka Remote instrumentation" should {
68 | "propagate the current Context when creating a new remote actor" in {
69 | Kamon.runWithContext(contextWithNameTag("deploy-remote-actor-1")) {
70 | system.actorOf(ContextEchoActor.remoteProps(Some(testActor), RemoteSystemAddress), "remote-deploy-fixture")
71 | }
72 |
73 | expectMsg(10 seconds, "name=deploy-remote-actor-1")
74 | }
75 |
76 |
77 | "propagate the Context when sending a message to a remotely deployed actor" in {
78 | val remoteRef = system.actorOf(ContextEchoActor.remoteProps(None, RemoteSystemAddress), "remote-message-fixture")
79 |
80 | Kamon.runWithContext(contextWithNameTag("message-remote-actor-1")) {
81 | remoteRef ! "reply-trace-token"
82 | }
83 | expectMsg("name=message-remote-actor-1")
84 | }
85 |
86 |
87 | "propagate the current Context when pipe or ask a message to a remotely deployed actor" in {
88 | implicit val ec = system.dispatcher
89 | implicit val askTimeout = Timeout(10 seconds)
90 | val remoteRef = system.actorOf(ContextEchoActor.remoteProps(None, RemoteSystemAddress), "remote-ask-and-pipe-fixture")
91 |
92 | Kamon.runWithContext(contextWithNameTag("ask-and-pipe-remote-actor-1")) {
93 | (remoteRef ? "reply-trace-token") pipeTo testActor
94 | }
95 |
96 | expectMsg("name=ask-and-pipe-remote-actor-1")
97 | }
98 |
99 |
100 | "propagate the current Context when sending a message to an ActorSelection" in {
101 | remoteSystem.actorOf(ContextEchoActor.props(None), "actor-selection-target-a")
102 | remoteSystem.actorOf(ContextEchoActor.props(None), "actor-selection-target-b")
103 | val selection = system.actorSelection(RemoteSystemAddress + "/user/actor-selection-target-*")
104 |
105 | Kamon.runWithContext(contextWithNameTag("message-remote-actor-selection-1")) {
106 | selection ! "reply-trace-token"
107 | }
108 |
109 | // one for each selected actor
110 | expectMsg("name=message-remote-actor-selection-1")
111 | expectMsg("name=message-remote-actor-selection-1")
112 | }
113 |
114 | "propagate the current Context when sending messages to remote routees of a router" in {
115 | remoteSystem.actorOf(ContextEchoActor.props(None), "router-target-a")
116 | remoteSystem.actorOf(ContextEchoActor.props(None), "router-target-b")
117 | val router = system.actorOf(RoundRobinGroup(List(
118 | RemoteSystemAddress + "/user/router-target-a",
119 | RemoteSystemAddress + "/user/router-target-b"
120 | )).props(), "router")
121 |
122 | Kamon.runWithContext(contextWithNameTag("remote-routee-1")) {
123 | router ! "reply-trace-token"
124 | }
125 |
126 | expectMsg("name=remote-routee-1")
127 | }
128 |
129 | "propagate the current Context when a remotely supervised child fails" in {
130 | val supervisor = system.actorOf(Props(new SupervisorOfRemote(testActor, RemoteSystemAddress)),"SUPERVISOR")
131 |
132 | Kamon.runWithContext(contextWithNameTag("remote-supervision-1")) {
133 | supervisor ! "fail"
134 | }
135 |
136 | expectMsg(2 minutes,"name=remote-supervision-1")
137 | }
138 |
139 | "record in/out message counts and sizes for both sending and receiving side" in {
140 | val (out, in) = (
141 | AkkaRemoteMetrics.OutboundMessageSize.withTags(TagSet.of("system", system.name)).distribution(false),
142 | AkkaRemoteMetrics.OutboundMessageSize.withTags(TagSet.of("system", system.name)).distribution(false)
143 | )
144 |
145 | assert(out.max > 0)
146 | assert(in.max > 0)
147 | assert(out.count > 0)
148 | assert(in.count > 0)
149 | }
150 |
151 | "record de/serialization times for messages" in {
152 | val systems = Seq(system.name, remoteSystem.name)
153 | val serializationTimes = systems.map(s => SerializationTime.withTags(TagSet.of("system", s)).distribution().count)
154 | val deserializationTimes = systems.map(s => DeserializationTime.withTags(TagSet.of("system", s)).distribution().count)
155 |
156 | forAll(serializationTimes ++ deserializationTimes) { count => assert(count > 0) }
157 | }
158 | }
159 | }
160 |
161 |
--------------------------------------------------------------------------------
/test/akka-2.5/src/test/scala/kamon/instrumentation/akka/remote/ArteryTcpRemotingInstrumentationSpec.scala:
--------------------------------------------------------------------------------
1 | package kamon.instrumentation.akka.remote
2 |
3 | import akka.actor._
4 | import akka.pattern.{ask, pipe}
5 | import akka.routing.RoundRobinGroup
6 | import akka.testkit.{ImplicitSender, TestKitBase}
7 | import akka.util.Timeout
8 | import com.typesafe.config.ConfigFactory
9 | import kamon.Kamon
10 | import kamon.context.Context
11 | import kamon.instrumentation.akka.AkkaRemoteMetrics.{DeserializationTime, SerializationTime}
12 | import kamon.instrumentation.akka.{AkkaRemoteMetrics, ContextEchoActor}
13 | import kamon.tag.TagSet
14 | import kamon.testkit.{InstrumentInspection, MetricInspection}
15 | import org.scalatest.Inspectors._
16 | import org.scalatest.{Matchers, WordSpecLike}
17 |
18 | import scala.concurrent.duration._
19 |
20 | class ArteryTcpRemotingInstrumentationSpec extends TestKitBase with WordSpecLike with Matchers with ImplicitSender
21 | with MetricInspection.Syntax with InstrumentInspection.Syntax {
22 |
23 | implicit lazy val system: ActorSystem = {
24 | ActorSystem("remoting-spec-local-system", ConfigFactory.parseString(
25 | """
26 | |akka {
27 | | actor {
28 | | provider = cluster
29 | | }
30 | |
31 | | remote {
32 | | artery {
33 | | enabled = on
34 | | transport = tcp
35 | | canonical.hostname = "127.0.0.1"
36 | | canonical.port = 2554
37 | | }
38 | | }
39 | |}
40 | """.stripMargin))
41 | }
42 |
43 | val remoteSystem: ActorSystem = ActorSystem("remoting-spec-remote-system", ConfigFactory.parseString(
44 | """
45 | |akka {
46 | | actor {
47 | | provider = cluster
48 | | }
49 | |
50 | | remote {
51 | | artery {
52 | | enabled = on
53 | | transport = tcp
54 | | canonical.hostname = "127.0.0.1"
55 | | canonical.port = 2555
56 | | }
57 | | }
58 | |}
59 | """.stripMargin))
60 |
61 | val RemoteSystemAddress = AddressFromURIString("akka://remoting-spec-remote-system@127.0.0.1:2555")
62 |
63 | def contextWithBroadcast(name: String): Context =
64 | Context.Empty.withTag(
65 | ContextEchoActor.EchoTag,
66 | name
67 | )
68 |
69 | "The Akka Remote instrumentation" should {
70 | "propagate the current Context when creating a new remote actor" in {
71 | val a = Kamon.runWithContext(contextWithBroadcast("deploy-remote-actor-1")) {
72 | system.actorOf(ContextEchoActor.remoteProps(Some(testActor), RemoteSystemAddress), "remote-deploy-fixture")
73 | }
74 |
75 | expectMsg(10 seconds, "name=deploy-remote-actor-1")
76 | }
77 |
78 |
79 | "propagate the Context when sending a message to a remotely deployed actor" in {
80 | val remoteRef = system.actorOf(ContextEchoActor.remoteProps(None, RemoteSystemAddress), "remote-message-fixture")
81 |
82 | Kamon.runWithContext(contextWithBroadcast("message-remote-actor-1")) {
83 | remoteRef ! "reply-trace-token"
84 | }
85 | expectMsg("name=message-remote-actor-1")
86 | }
87 |
88 |
89 | "propagate the current Context when pipe or ask a message to a remotely deployed actor" in {
90 | implicit val ec = system.dispatcher
91 | implicit val askTimeout = Timeout(10 seconds)
92 | val remoteRef = system.actorOf(ContextEchoActor.remoteProps(None, RemoteSystemAddress), "remote-ask-and-pipe-fixture")
93 |
94 | Kamon.runWithContext(contextWithBroadcast("ask-and-pipe-remote-actor-1")) {
95 | (remoteRef ? "reply-trace-token") pipeTo testActor
96 | }
97 |
98 | expectMsg("name=ask-and-pipe-remote-actor-1")
99 | }
100 |
101 |
102 | "propagate the current Context when sending a message to an ActorSelection" in {
103 | remoteSystem.actorOf(ContextEchoActor.props(None), "actor-selection-target-a")
104 | remoteSystem.actorOf(ContextEchoActor.props(None), "actor-selection-target-b")
105 | val selection = system.actorSelection(RemoteSystemAddress + "/user/actor-selection-target-*")
106 |
107 | Kamon.runWithContext(contextWithBroadcast("message-remote-actor-selection-1")) {
108 | selection ! "reply-trace-token"
109 | }
110 |
111 | // one for each selected actor
112 | expectMsg("name=message-remote-actor-selection-1")
113 | expectMsg("name=message-remote-actor-selection-1")
114 | }
115 |
116 | "propagate the current Context when sending messages to remote routees of a router" in {
117 | remoteSystem.actorOf(ContextEchoActor.props(None), "router-target-a")
118 | remoteSystem.actorOf(ContextEchoActor.props(None), "router-target-b")
119 | val router = system.actorOf(RoundRobinGroup(List(
120 | RemoteSystemAddress + "/user/router-target-a",
121 | RemoteSystemAddress + "/user/router-target-b"
122 | )).props(), "router")
123 |
124 | Kamon.runWithContext(contextWithBroadcast("remote-routee-1")) {
125 | router ! "reply-trace-token"
126 | }
127 |
128 | expectMsg("name=remote-routee-1")
129 | }
130 |
131 | "propagate the current Context when a remotely supervised child fails" in {
132 | val supervisor = system.actorOf(Props(new SupervisorOfRemote(testActor, RemoteSystemAddress)),"SUPERVISOR")
133 |
134 | Kamon.runWithContext(contextWithBroadcast("remote-supervision-1")) {
135 | supervisor ! "fail"
136 | }
137 |
138 | expectMsg(2 minutes,"name=remote-supervision-1")
139 | }
140 |
141 | "record in/out message counts and sizes for both sending and receiving side" in {
142 | val (out, in) = (
143 | AkkaRemoteMetrics.OutboundMessageSize.withTags(TagSet.of("system", system.name)).distribution(false),
144 | AkkaRemoteMetrics.OutboundMessageSize.withTags(TagSet.of("system", system.name)).distribution(false)
145 | )
146 |
147 | assert(out.max > 0)
148 | assert(in.max > 0)
149 | assert(out.count > 0)
150 | assert(in.count > 0)
151 | }
152 |
153 | "record de/serialization times for messages" in {
154 | val systems = Seq(system.name, remoteSystem.name)
155 | val serializationTimes = systems.map(s => SerializationTime.withTags(TagSet.of("system", s)).distribution().count)
156 | val deserializationTimes = systems.map(s => DeserializationTime.withTags(TagSet.of("system", s)).distribution().count)
157 |
158 | forAll(serializationTimes ++ deserializationTimes) { count => assert(count > 0) }
159 | }
160 | }
161 | }
162 |
163 |
--------------------------------------------------------------------------------
/test/common/src/test/scala/kamon/instrumentation/akka/ActorMetricsSpec.scala:
--------------------------------------------------------------------------------
1 | /* =========================================================================================
2 | * Copyright © 2013 the kamon project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5 | * except in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the
10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
11 | * either express or implied. See the License for the specific language governing permissions
12 | * and limitations under the License.
13 | * =========================================================================================
14 | */
15 |
16 | package kamon.instrumentation.akka
17 |
18 | import akka.actor._
19 | import akka.testkit.{ImplicitSender, TestKit, TestProbe}
20 | import com.typesafe.config.ConfigFactory
21 | import kamon.Kamon
22 | import ActorMetricsTestActor._
23 | import kamon.instrumentation.akka.AkkaMetrics._
24 | import kamon.tag.TagSet
25 | import kamon.testkit.{InstrumentInspection, MetricInspection}
26 | import org.scalactic.TimesOnInt._
27 | import org.scalatest.concurrent.Eventually
28 | import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike}
29 |
30 | import scala.concurrent.duration._
31 |
32 |
33 | class ActorMetricsSpec extends TestKit(ActorSystem("ActorMetricsSpec")) with WordSpecLike with MetricInspection.Syntax with InstrumentInspection.Syntax with Matchers
34 | with BeforeAndAfterAll with ImplicitSender with Eventually {
35 |
36 | "the Kamon actor metrics" should {
37 | "respect the configured include and exclude filters" in new ActorMetricsFixtures {
38 | val trackedActor = createTestActor("tracked-actor")
39 | ActorProcessingTime.tagValues("path") should contain("ActorMetricsSpec/user/tracked-actor")
40 |
41 | val nonTrackedActor = createTestActor("non-tracked-actor")
42 | ActorProcessingTime.tagValues("path") shouldNot contain("ActorMetricsSpec/user/non-tracked-actor")
43 |
44 | val trackedButExplicitlyExcluded = createTestActor("tracked-explicitly-excluded")
45 | ActorProcessingTime.tagValues("path") shouldNot contain("ActorMetricsSpec/user/tracked-explicitly-excluded")
46 | }
47 |
48 | "not pick up the root supervisor" in {
49 | ActorProcessingTime.tagValues("path") shouldNot contain("ActorMetricsSpec/")
50 | }
51 |
52 | "record the processing-time of the receive function" in new ActorMetricsFixtures {
53 | createTestActor("measuring-processing-time", true) ! TrackTimings(sleep = Some(100 millis))
54 |
55 | val timings = expectMsgType[TrackedTimings]
56 | val processingTimeDistribution = ActorProcessingTime.
57 | withTags(actorTags("ActorMetricsSpec/user/measuring-processing-time")).distribution()
58 |
59 | processingTimeDistribution.count should be(1L)
60 | processingTimeDistribution.buckets.size should be(1L)
61 | processingTimeDistribution.buckets.head.value should be(timings.approximateProcessingTime +- 10.millis.toNanos)
62 | }
63 |
64 | "record the number of errors" in new ActorMetricsFixtures {
65 | val trackedActor = createTestActor("measuring-errors")
66 | 10.times(trackedActor ! Fail)
67 |
68 | trackedActor ! Ping
69 | expectMsg(Pong)
70 | ActorErrors.withTags(actorTags("ActorMetricsSpec/user/measuring-errors")).value() should be(10)
71 | }
72 |
73 | "record the mailbox-size" in new ActorMetricsFixtures {
74 | val trackedActor = createTestActor("measuring-mailbox-size", true)
75 | trackedActor ! TrackTimings(sleep = Some(1 second))
76 | 10.times(trackedActor ! Discard)
77 | trackedActor ! Ping
78 |
79 | val timings = expectMsgType[TrackedTimings]
80 | expectMsg(Pong)
81 |
82 | val mailboxSizeDistribution = ActorMailboxSize
83 | .withTags(actorTags("ActorMetricsSpec/user/measuring-mailbox-size")).distribution()
84 |
85 | mailboxSizeDistribution.min should be(0L +- 1L)
86 | mailboxSizeDistribution.max should be(11L +- 1L)
87 | }
88 |
89 | "record the time-in-mailbox" in new ActorMetricsFixtures {
90 | val trackedActor = createTestActor("measuring-time-in-mailbox", true)
91 | trackedActor ! TrackTimings(sleep = Some(100 millis))
92 | val timings = expectMsgType[TrackedTimings]
93 |
94 | val timeInMailboxDistribution = ActorTimeInMailbox
95 | .withTags(actorTags("ActorMetricsSpec/user/measuring-time-in-mailbox")).distribution()
96 |
97 | timeInMailboxDistribution.count should be(1L)
98 | timeInMailboxDistribution.buckets.head.frequency should be(1L)
99 | timeInMailboxDistribution.buckets.head.value should be(timings.approximateTimeInMailbox +- 10.millis.toNanos)
100 | }
101 |
102 | "clean up the associated recorder when the actor is stopped" in new ActorMetricsFixtures {
103 | val trackedActor = createTestActor("stop")
104 |
105 | // Killing the actor should remove it's ActorMetrics and registering again bellow should create a new one.
106 | val deathWatcher = TestProbe()
107 | deathWatcher.watch(trackedActor)
108 | trackedActor ! PoisonPill
109 | deathWatcher.expectTerminated(trackedActor)
110 |
111 | eventually(timeout(1 second)) {
112 | ActorProcessingTime.tagValues("path") shouldNot contain("ActorMetricsSpec/user/stop")
113 | }
114 | }
115 | }
116 |
117 |
118 | override protected def afterAll(): Unit = shutdown()
119 |
120 | def actorTags(path: String): TagSet =
121 | TagSet.from(
122 | Map(
123 | "path" -> path,
124 | "system" -> "ActorMetricsSpec",
125 | "dispatcher" -> "akka.actor.default-dispatcher",
126 | "class" -> "kamon.instrumentation.akka.ActorMetricsTestActor"
127 | )
128 | )
129 |
130 | trait ActorMetricsFixtures {
131 |
132 | def createTestActor(name: String, resetState: Boolean = false): ActorRef = {
133 | val actor = system.actorOf(Props[ActorMetricsTestActor], name)
134 | val initialiseListener = TestProbe()
135 |
136 | // Ensure that the router has been created before returning.
137 | actor.tell(Ping, initialiseListener.ref)
138 | initialiseListener.expectMsg(Pong)
139 |
140 | // Cleanup all the metric recording instruments:
141 | if(resetState) {
142 | val tags = actorTags(s"ActorMetricsSpec/user/$name")
143 |
144 | ActorTimeInMailbox.withTags(tags).distribution(resetState = true)
145 | ActorProcessingTime.withTags(tags).distribution(resetState = true)
146 | ActorMailboxSize.withTags(tags).distribution(resetState = true)
147 | ActorErrors.withTags(tags).value(resetState = true)
148 | }
149 |
150 | actor
151 | }
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/instrumentation/akka-2.5/src/main/scala/kamon/instrumentation/akka/instrumentations/akka_25/remote/RemotingInstrumentation.scala:
--------------------------------------------------------------------------------
1 | package kamon.instrumentation.akka.instrumentations.akka_25.remote
2 |
3 | import akka.actor.ActorSystem
4 | import akka.remote.kamon.instrumentation.akka.instrumentations.akka_25.remote.{ArteryMessageDispatcherAdvice, CaptureContextOnInboundEnvelope, DeserializeForArteryAdvice, SerializeForArteryAdvice}
5 | import akka.kamon.instrumentation.akka.instrumentations.akka_25.remote.{AkkaPduProtobufCodecConstructMessageMethodInterceptor, AkkaPduProtobufCodecDecodeMessage}
6 | import kamon.Kamon
7 | import kamon.context.Storage
8 | import kamon.context.Storage.Scope
9 | import kamon.instrumentation.akka.AkkaRemoteInstrumentation
10 | import kamon.instrumentation.akka.AkkaRemoteMetrics.SerializationInstruments
11 | import kamon.instrumentation.akka.instrumentations.{AkkaPrivateAccess, VersionFiltering}
12 | import kamon.instrumentation.context.{CaptureCurrentContextOnExit, HasContext}
13 | import kanela.agent.api.instrumentation.InstrumentationBuilder
14 | import kanela.agent.libs.net.bytebuddy.asm.Advice
15 |
16 |
17 | class RemotingInstrumentation extends InstrumentationBuilder with VersionFiltering {
18 |
19 | onAkka("2.4", "2.5") {
20 |
21 | /**
22 | * Send messages might be buffered if they reach the EndpointWriter before it has been initialized and the current
23 | * Context might be lost after the buffering, so we make sure we capture the context when the Send command was
24 | * created and then apply it during the EndpointWrite.writeSend method execution (see bellow).
25 | */
26 | onType("akka.remote.EndpointManager$Send")
27 | .mixin(classOf[HasContext.Mixin])
28 | .advise(isConstructor, CaptureCurrentContextOnExit)
29 |
30 | onType("akka.remote.EndpointWriter")
31 | .advise(method("writeSend"), WriteSendWithContext)
32 |
33 | /**
34 | * Reads and writes the Akka PDU using a modified version of the Protobuf that has an extra field for a Context
35 | * instance.
36 | */
37 | onType("akka.remote.transport.AkkaPduProtobufCodec$")
38 | .intercept(method("constructMessage"), new AkkaPduProtobufCodecConstructMessageMethodInterceptor())
39 | .advise(method("decodeMessage"), classOf[AkkaPduProtobufCodecDecodeMessage])
40 |
41 | /**
42 | * Mixin Serialization Instruments to the Actor System and use them to record the serialization and deserialization
43 | * time metrics.
44 | */
45 | onType("akka.actor.ActorSystemImpl")
46 | .mixin(classOf[HasSerializationInstruments.Mixin])
47 | .advise(isConstructor, InitializeActorSystemAdvice)
48 |
49 | onType("akka.remote.MessageSerializer$")
50 | .advise(method("serialize"), MeasureSerializationTime)
51 | .advise(method("deserialize"), MeasureDeserializationTime)
52 |
53 | /**
54 | * Artery
55 | */
56 | onType("akka.remote.artery.ReusableOutboundEnvelope")
57 | .mixin(classOf[HasContext.Mixin])
58 | .advise(method("copy"), CopyContextOnReusableEnvelope)
59 |
60 | onType("akka.remote.artery.Association")
61 | .advise(method("createOutboundEnvelope$1"), CaptureCurrentContextOnReusableEnvelope)
62 |
63 | onType("akka.remote.MessageSerializer$")
64 | .advise(method("serializeForArtery"), classOf[SerializeForArteryAdvice])
65 | .advise(method("deserializeForArtery"), classOf[DeserializeForArteryAdvice])
66 |
67 | onType("akka.remote.artery.ReusableInboundEnvelope")
68 | .mixin(classOf[HasContext.Mixin])
69 | .advise(method("withMessage"), classOf[CaptureContextOnInboundEnvelope])
70 | .advise(method("copyForLane"), CopyContextOnReusableEnvelope)
71 |
72 | onType("akka.remote.artery.MessageDispatcher")
73 | .advise(method("dispatch"), classOf[ArteryMessageDispatcherAdvice])
74 | }
75 |
76 | }
77 |
78 |
79 | object CopyContextOnReusableEnvelope {
80 |
81 | @Advice.OnMethodExit
82 | def exit(@Advice.This oldEnvelope: Any, @Advice.Return newEnvelope: Any): Unit =
83 | newEnvelope.asInstanceOf[HasContext].setContext(oldEnvelope.asInstanceOf[HasContext].context)
84 | }
85 |
86 | object CaptureCurrentContextOnReusableEnvelope {
87 |
88 | @Advice.OnMethodExit
89 | def exit(@Advice.Return envelope: Any): Unit = {
90 | envelope.asInstanceOf[HasContext].setContext(Kamon.currentContext())
91 | }
92 | }
93 |
94 | object WriteSendWithContext {
95 |
96 | @Advice.OnMethodEnter
97 | def enter(@Advice.Argument(0) send: Any): Scope = {
98 | Kamon.storeContext(send.asInstanceOf[HasContext].context)
99 | }
100 |
101 | @Advice.OnMethodExit
102 | def exit(@Advice.Enter scope: Scope): Unit = {
103 | scope.asInstanceOf[Scope].close()
104 | }
105 | }
106 |
107 | trait HasSerializationInstruments {
108 | def serializationInstruments: SerializationInstruments
109 | def setSerializationInstruments(instruments: SerializationInstruments): Unit
110 | }
111 |
112 | object HasSerializationInstruments {
113 |
114 | class Mixin(var serializationInstruments: SerializationInstruments) extends HasSerializationInstruments {
115 | override def setSerializationInstruments(instruments: SerializationInstruments): Unit =
116 | serializationInstruments = instruments
117 | }
118 | }
119 |
120 | object InitializeActorSystemAdvice {
121 |
122 | @Advice.OnMethodExit
123 | def exit(@Advice.This system: ActorSystem with HasSerializationInstruments): Unit =
124 | system.setSerializationInstruments(new SerializationInstruments(system.name))
125 |
126 | }
127 |
128 | object MeasureSerializationTime {
129 |
130 | @Advice.OnMethodEnter
131 | def enter(): Long = {
132 | if(AkkaRemoteInstrumentation.settings().trackSerializationMetrics) System.nanoTime() else 0L
133 | }
134 |
135 | @Advice.OnMethodExit
136 | def exit(@Advice.Argument(0) system: AnyRef, @Advice.Enter startNanoTime: Long): Unit = {
137 | if(startNanoTime != 0L) {
138 | system.asInstanceOf[HasSerializationInstruments]
139 | .serializationInstruments
140 | .serializationTime
141 | .record(System.nanoTime() - startNanoTime)
142 | }
143 | }
144 | }
145 |
146 | object MeasureDeserializationTime {
147 |
148 | @Advice.OnMethodEnter
149 | def enter(): Long = {
150 | if(AkkaRemoteInstrumentation.settings().trackSerializationMetrics) System.nanoTime() else 0L
151 | }
152 |
153 | @Advice.OnMethodExit
154 | def exit(@Advice.Argument(0) system: AnyRef, @Advice.Enter startNanoTime: Long, @Advice.Return msg: Any): Unit = {
155 |
156 | if(AkkaPrivateAccess.isSystemMessage(msg)) {
157 | msg match {
158 | case hc: HasContext if hc.context == null =>
159 | hc.setContext(Kamon.currentContext())
160 | case _ =>
161 | }
162 | }
163 |
164 | if(startNanoTime != 0L) {
165 | system.asInstanceOf[HasSerializationInstruments]
166 | .serializationInstruments
167 | .deserializationTime
168 | .record(System.nanoTime() - startNanoTime)
169 | }
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/instrumentation/common/src/main/scala/kamon/instrumentation/akka/instrumentations/ActorInstrumentation.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * =========================================================================================
3 | * Copyright © 2013-2018 the kamon project
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
6 | * except in compliance with the License. You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software distributed under the
11 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
12 | * either express or implied. See the License for the specific language governing permissions
13 | * and limitations under the License.
14 | * =========================================================================================
15 | */
16 |
17 | package kamon.instrumentation.akka.instrumentations
18 |
19 | import akka.actor.{ActorRef, ActorSystem}
20 | import akka.instrumentation.ReplaceWithMethodInterceptor
21 |
22 | import kamon.Kamon
23 | import kamon.context.Storage.Scope
24 | import kamon.instrumentation.akka.instrumentations.HasActorMonitor.actorMonitor
25 | import kamon.instrumentation.context.{HasContext, HasTimestamp}
26 | import kanela.agent.api.instrumentation.InstrumentationBuilder
27 | import kanela.agent.libs.net.bytebuddy.asm.Advice
28 | import kanela.agent.libs.net.bytebuddy.asm.Advice.{Argument, OnMethodEnter, OnMethodExit, This}
29 |
30 | import scala.util.Properties
31 |
32 | class ActorInstrumentation extends InstrumentationBuilder {
33 |
34 | /**
35 | * This is where most of the Actor processing magic happens. Handling of messages, errors and system messages.
36 | */
37 | onType("akka.actor.ActorCell")
38 | .mixin(classOf[HasActorMonitor.Mixin])
39 | .advise(isConstructor, ActorCellConstructorAdvice)
40 | .advise(method("invoke"), classOf[ActorCellInvokeAdvice])
41 | .advise(method("handleInvokeFailure"), HandleInvokeFailureMethodAdvice)
42 | .advise(method("sendMessage").and(takesArguments(1)), SendMessageAdvice)
43 | .advise(method("terminate"), TerminateMethodAdvice)
44 | .advise(method("swapMailbox"), ActorCellSwapMailboxAdvice)
45 | .advise(method("invokeAll$1"), InvokeAllMethodInterceptor)
46 |
47 | /**
48 | * Ensures that the Context is properly propagated when messages are temporarily stored on an UnstartedCell.
49 | */
50 | onType("akka.actor.UnstartedCell")
51 | .mixin(classOf[HasActorMonitor.Mixin])
52 | .advise(isConstructor, RepointableActorCellConstructorAdvice)
53 | .advise(method("sendMessage").and(takesArguments(1)), SendMessageAdvice)
54 | .intercept(method("replaceWith"), ReplaceWithMethodInterceptor)
55 |
56 | }
57 |
58 | object ActorInstrumentation {
59 |
60 | val (unstartedCellQueueField, unstartedCellLockField, systemMsgQueueField) = {
61 | val unstartedCellClass = AkkaPrivateAccess.unstartedActorCellClass()
62 |
63 | val prefix = Properties.versionNumberString.split("\\.").take(2).mkString(".") match {
64 | case _@ "2.11" => "akka$actor$UnstartedCell$$"
65 | case _@ "2.12" => ""
66 | case _@ "2.13" => ""
67 | case v => throw new IllegalStateException(s"Incompatible Scala version: $v")
68 | }
69 |
70 | val queueField = unstartedCellClass.getDeclaredField(prefix+"queue")
71 | val lockField = unstartedCellClass.getDeclaredField("lock")
72 | val sysQueueField = unstartedCellClass.getDeclaredField(prefix+"sysmsgQueue")
73 | queueField.setAccessible(true)
74 | lockField.setAccessible(true)
75 | sysQueueField.setAccessible(true)
76 |
77 | (queueField, lockField, sysQueueField)
78 | }
79 |
80 | }
81 |
82 | trait HasActorMonitor {
83 | def actorMonitor: ActorMonitor
84 | def setActorMonitor(actorMonitor: ActorMonitor): Unit
85 | }
86 |
87 | object HasActorMonitor {
88 |
89 | class Mixin(var actorMonitor: ActorMonitor) extends HasActorMonitor {
90 | override def setActorMonitor(actorMonitor: ActorMonitor): Unit =
91 | this.actorMonitor = actorMonitor
92 | }
93 |
94 | def actorMonitor(cell: Any): ActorMonitor =
95 | cell.asInstanceOf[HasActorMonitor].actorMonitor
96 | }
97 |
98 | object ActorCellSwapMailboxAdvice {
99 |
100 | @Advice.OnMethodEnter
101 | def enter(@Advice.This cell: Any, @Advice.Argument(0) newMailbox: Any): Boolean = {
102 | val isShuttingDown = AkkaPrivateAccess.isDeadLettersMailbox(cell, newMailbox)
103 | if(isShuttingDown)
104 | actorMonitor(cell).onTerminationStart()
105 |
106 | isShuttingDown
107 | }
108 |
109 | @Advice.OnMethodExit
110 | def exit(@Advice.This cell: Any, @Advice.Return oldMailbox: Any, @Advice.Enter isShuttingDown: Boolean): Unit = {
111 | if(oldMailbox != null && isShuttingDown) {
112 | actorMonitor(cell).onDroppedMessages(AkkaPrivateAccess.mailboxMessageCount(oldMailbox))
113 | }
114 | }
115 | }
116 |
117 | object InvokeAllMethodInterceptor {
118 |
119 | @Advice.OnMethodEnter
120 | def enter(@Advice.Argument(0) message: Any): Option[Scope] =
121 | message match {
122 | case m: HasContext => Some(Kamon.storeContext(m.context))
123 | case _ => None
124 | }
125 |
126 | @Advice.OnMethodExit
127 | def exit(@Advice.Enter scope: Option[Scope]): Unit =
128 | scope.foreach(_.close())
129 | }
130 |
131 | object SendMessageAdvice {
132 |
133 | @OnMethodEnter(suppress = classOf[Throwable])
134 | def onEnter(@This cell: Any, @Argument(0) envelope: Object): Unit = {
135 |
136 | val instrumentation = actorMonitor(cell)
137 | envelope.asInstanceOf[HasContext].setContext(instrumentation.captureEnvelopeContext())
138 | envelope.asInstanceOf[HasTimestamp].setTimestamp(instrumentation.captureEnvelopeTimestamp())
139 | }
140 | }
141 |
142 | object RepointableActorCellConstructorAdvice {
143 |
144 | @Advice.OnMethodExit(suppress = classOf[Throwable])
145 | def onExit(@This cell: Any, @Argument(0) system: ActorSystem, @Argument(1) ref: ActorRef, @Argument(3) parent: ActorRef): Unit =
146 | cell.asInstanceOf[HasActorMonitor].setActorMonitor(ActorMonitor.from(cell, ref, parent, system))
147 | }
148 |
149 | object ActorCellConstructorAdvice {
150 |
151 | @OnMethodExit(suppress = classOf[Throwable])
152 | def onExit(@This cell: Any, @Argument(0) system: ActorSystem, @Argument(1) ref: ActorRef, @Argument(4) parent: ActorRef): Unit =
153 | cell.asInstanceOf[HasActorMonitor].setActorMonitor(ActorMonitor.from(cell, ref, parent, system))
154 | }
155 |
156 | object HandleInvokeFailureMethodAdvice {
157 |
158 | @OnMethodEnter(suppress = classOf[Throwable])
159 | def onEnter(@This cell: Any, @Argument(1) failure: Throwable): Unit =
160 | actorMonitor(cell).onFailure(failure)
161 |
162 | }
163 |
164 | object TerminateMethodAdvice {
165 |
166 | @OnMethodEnter(suppress = classOf[Throwable])
167 | def onEnter(@This cell: Any): Unit = {
168 | actorMonitor(cell).cleanup()
169 |
170 | if (AkkaPrivateAccess.isRoutedActorCell(cell)) {
171 | cell.asInstanceOf[HasRouterMonitor].routerMonitor.cleanup()
172 | }
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/instrumentation/akka-2.6/src/main/scala/kamon/instrumentation/akka/instrumentations/akka_26/DispatcherInstrumentation.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * =========================================================================================
3 | * Copyright © 2013-2018 the kamon project
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
6 | * except in compliance with the License. You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software distributed under the
11 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
12 | * either express or implied. See the License for the specific language governing permissions
13 | * and limitations under the License.
14 | * =========================================================================================
15 | */
16 |
17 | package kamon.instrumentation.akka.instrumentations.akka_26
18 |
19 | import java.util.concurrent.{AbstractExecutorService, Callable, ExecutorService, ThreadFactory, TimeUnit}
20 |
21 | import akka.dispatch.{DefaultExecutorServiceConfigurator, DispatcherPrerequisites, Dispatchers, ExecutorServiceFactory, ExecutorServiceFactoryProvider, ForkJoinExecutorConfigurator, PinnedDispatcherConfigurator, ThreadPoolExecutorConfigurator}
22 | import kamon.instrumentation.akka.instrumentations.VersionFiltering
23 | import kamon.Kamon
24 | import kamon.instrumentation.akka.AkkaInstrumentation
25 | import kamon.instrumentation.akka.instrumentations.DispatcherInfo.{HasDispatcherName, HasDispatcherPrerequisites}
26 | import kamon.instrumentation.executor.ExecutorInstrumentation
27 | import kamon.tag.TagSet
28 | import kanela.agent.api.instrumentation.InstrumentationBuilder
29 | import kanela.agent.libs.net.bytebuddy.asm.Advice
30 | import kanela.agent.libs.net.bytebuddy.implementation.bind.annotation.{Argument, SuperCall, This}
31 |
32 | class DispatcherInstrumentation extends InstrumentationBuilder with VersionFiltering {
33 |
34 | onAkka("2.6") {
35 |
36 | /**
37 | * This is where the actual ExecutorService instances are being created, but at this point we don't have access to
38 | * the Actor System Name nor the Dispatcher name, which is why there is additional instrumentation to carry these two
39 | * names down to the ExecutorServiceFactory and use them to tag the newly instrumented ExecutorService.
40 | */
41 | onSubTypesOf("akka.dispatch.ExecutorServiceFactory")
42 | .mixin(classOf[HasDispatcherPrerequisites.Mixin])
43 | .mixin(classOf[HasDispatcherName.Mixin])
44 | .intercept(method("createExecutorService"), InstrumentNewExecutorServiceOnAkka26)
45 |
46 | /**
47 | * First step on getting the Actor System name is to read it from the prerequisites instance passed to the
48 | * constructors of these two classes.
49 | */
50 | onTypes(
51 | "akka.dispatch.ThreadPoolExecutorConfigurator",
52 | "akka.dispatch.ForkJoinExecutorConfigurator",
53 | "akka.dispatch.PinnedDispatcherConfigurator",
54 | "akka.dispatch.DefaultExecutorServiceConfigurator")
55 | .mixin(classOf[HasDispatcherPrerequisites.Mixin])
56 | .advise(isConstructor, CaptureDispatcherPrerequisitesOnExecutorConfigurator)
57 |
58 | /**
59 | * Copies the Actor System and Dispatcher names to the ExecutorServiceFactory instances for the two types of
60 | * executors instrumented by Kamon.
61 | */
62 | onTypes(
63 | "akka.dispatch.ThreadPoolConfig",
64 | "akka.dispatch.ForkJoinExecutorConfigurator",
65 | "akka.dispatch.PinnedDispatcherConfigurator",
66 | "akka.dispatch.DefaultExecutorServiceConfigurator")
67 | .mixin(classOf[HasDispatcherName.Mixin])
68 | .advise(method("createExecutorServiceFactory"), CopyDispatcherInfoToExecutorServiceFactory)
69 |
70 | /**
71 | * This ensures that the ActorSystem name is not lost when creating PinnedDispatcher instances.
72 | */
73 | onType("akka.dispatch.ThreadPoolConfig")
74 | .mixin(classOf[HasDispatcherPrerequisites.Mixin])
75 | .advise(method("copy"), ThreadPoolConfigCopyAdvice)
76 | }
77 |
78 | }
79 |
80 | object CaptureDispatcherPrerequisitesOnExecutorConfigurator {
81 |
82 | @Advice.OnMethodExit(suppress = classOf[Throwable])
83 | def exit(@Advice.This configurator: Any, @Advice.Argument(1) prerequisites: DispatcherPrerequisites): Unit = {
84 | configurator match {
85 | case fjec: ForkJoinExecutorConfigurator => fjec.asInstanceOf[HasDispatcherPrerequisites].setDispatcherPrerequisites(prerequisites)
86 | case tpec: ThreadPoolExecutorConfigurator => tpec.threadPoolConfig.asInstanceOf[HasDispatcherPrerequisites].setDispatcherPrerequisites(prerequisites)
87 | case pdc: PinnedDispatcherConfigurator => pdc.asInstanceOf[HasDispatcherPrerequisites].setDispatcherPrerequisites(prerequisites)
88 | case desc: DefaultExecutorServiceConfigurator => desc.asInstanceOf[HasDispatcherPrerequisites].setDispatcherPrerequisites(prerequisites)
89 | case _ => // just ignore any other case.
90 | }
91 | }
92 | }
93 |
94 | object CopyDispatcherInfoToExecutorServiceFactory {
95 |
96 | @Advice.OnMethodExit
97 | def exit(@Advice.This poolConfig: HasDispatcherPrerequisites, @Advice.Argument(0) dispatcherName: String, @Advice.Return factory: Any): Unit = {
98 | val factoryWithMixins = factory.asInstanceOf[HasDispatcherName with HasDispatcherPrerequisites]
99 | factoryWithMixins.setDispatcherPrerequisites(poolConfig.dispatcherPrerequisites)
100 | factoryWithMixins.setDispatcherName(dispatcherName)
101 | }
102 | }
103 |
104 | object InstrumentNewExecutorServiceOnAkka26 {
105 |
106 | def around(@This factory: HasDispatcherPrerequisites with HasDispatcherName, @SuperCall callable: Callable[ExecutorService]): ExecutorService = {
107 | val executor = callable.call()
108 | val dispatcherName = factory.dispatcherName
109 | val systemTags = TagSet.of("akka.system", factory.dispatcherPrerequisites.settings.name)
110 |
111 | if(Kamon.filter(AkkaInstrumentation.TrackDispatcherFilterName).accept(dispatcherName)) {
112 | val defaultEcOption = factory.dispatcherPrerequisites.defaultExecutionContext
113 |
114 | if(dispatcherName == Dispatchers.DefaultDispatcherId && defaultEcOption.isDefined) {
115 | ExecutorInstrumentation.instrumentExecutionContext(defaultEcOption.get, dispatcherName, systemTags)
116 | .underlyingExecutor.getOrElse(executor)
117 | } else {
118 | ExecutorInstrumentation.instrument(executor, dispatcherName, systemTags)
119 | }
120 | } else executor
121 | }
122 | }
123 |
124 | object ThreadPoolConfigCopyAdvice {
125 |
126 | @Advice.OnMethodExit
127 | def exit(@Advice.This original: Any, @Advice.Return copy: Any): Unit = {
128 | copy.asInstanceOf[HasDispatcherPrerequisites].setDispatcherPrerequisites(original.asInstanceOf[HasDispatcherPrerequisites].dispatcherPrerequisites)
129 | copy.asInstanceOf[HasDispatcherName].setDispatcherName(original.asInstanceOf[HasDispatcherName].dispatcherName)
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/instrumentation/common/src/main/scala/kamon/instrumentation/akka/remote/ShardingInstrumentation.scala:
--------------------------------------------------------------------------------
1 | package kamon.instrumentation.akka.remote
2 |
3 | import java.util.concurrent.atomic.AtomicLong
4 |
5 | import akka.actor.Actor
6 | import kamon.instrumentation.akka.AkkaClusterShardingMetrics.ShardingInstruments
7 | import kamon.instrumentation.akka.AkkaInstrumentation
8 | import kamon.instrumentation.akka.instrumentations.VersionFiltering
9 | import kamon.util.Filter
10 | import kanela.agent.api.instrumentation.InstrumentationBuilder
11 | import kanela.agent.libs.net.bytebuddy.asm.Advice
12 |
13 | class ShardingInstrumentation extends InstrumentationBuilder with VersionFiltering {
14 |
15 | onAkka("2.5") {
16 |
17 | /**
18 | * The ShardRegion instrumentation just takes care of counting the Region messages and, when stopped, cleans up the
19 | * instruments and Shard sampling schedules.
20 | */
21 | onType("akka.cluster.sharding.ShardRegion")
22 | .mixin(classOf[HasShardingInstruments.Mixin])
23 | .advise(isConstructor, InitializeShardRegionAdvice)
24 | .advise(method("deliverMessage"), DeliverMessageOnShardRegion)
25 | .advise(method("postStop"), RegionPostStopAdvice)
26 |
27 |
28 | /**
29 | * Shards control most of metrics generated by the module and we use the internal helper methods to know when
30 | * entities were created or shutdown as well as measuring how many messages were sent to them through each Shard. One
31 | * implementation note is that messages sent to unstarted entities are going to be counted more than once because
32 | * they will first by buffered and then delivered when the Entity is read, we are doing this to avoid having to double
33 | * the work done by "deliverMessage" (like calling he "extractEntityId" function on each message and determine
34 | * whether it should be buffered or forwarded).
35 | */
36 | onType("akka.cluster.sharding.Shard")
37 | .mixin(classOf[HasShardingInstruments.Mixin])
38 | .mixin(classOf[HasShardCounters.Mixin])
39 | .advise(isConstructor, InitializeShardAdvice)
40 | .advise(method("onLeaseAcquired"), ShardOnLeaseAcquiredAdvice)
41 | .advise(method("postStop"), ShardPostStopStoppedAdvice)
42 | .advise(method("getOrCreateEntity"), ShardGetOrCreateEntityAdvice)
43 | .advise(method("entityTerminated"), ShardEntityTerminatedAdvice)
44 | .advise(method("deliverMessage"), ShardDeliverMessageAdvice)
45 | }
46 |
47 | }
48 |
49 |
50 | trait HasShardingInstruments {
51 | def shardingInstruments: ShardingInstruments
52 | def setShardingInstruments(shardingInstruments: ShardingInstruments): Unit
53 | }
54 |
55 | object HasShardingInstruments {
56 |
57 | class Mixin(var shardingInstruments: ShardingInstruments) extends HasShardingInstruments {
58 | override def setShardingInstruments(shardingInstruments: ShardingInstruments): Unit =
59 | this.shardingInstruments = shardingInstruments
60 | }
61 | }
62 |
63 | trait HasShardCounters {
64 | def hostedEntitiesCounter: AtomicLong
65 | def processedMessagesCounter: AtomicLong
66 | def setCounters(hostedEntitiesCounter: AtomicLong, processedMessagesCounter: AtomicLong): Unit
67 | }
68 |
69 | object HasShardCounters {
70 |
71 | class Mixin(var hostedEntitiesCounter: AtomicLong, var processedMessagesCounter: AtomicLong) extends HasShardCounters {
72 | override def setCounters(hostedEntitiesCounter: AtomicLong, processedMessagesCounter: AtomicLong): Unit = {
73 | this.hostedEntitiesCounter = hostedEntitiesCounter
74 | this.processedMessagesCounter = processedMessagesCounter
75 | }
76 | }
77 | }
78 |
79 | object InitializeShardRegionAdvice {
80 |
81 | @Advice.OnMethodExit
82 | def exit(@Advice.This region: Actor with HasShardingInstruments, @Advice.Argument(0) typeName: String): Unit = {
83 | region.setShardingInstruments(new ShardingInstruments(region.context.system.name, typeName))
84 |
85 | val system = region.context.system
86 | val shardingGuardian = system.settings.config.getString("akka.cluster.sharding.guardian-name")
87 | val entitiesPath = s"${system.name}/system/$shardingGuardian/$typeName/*/*"
88 |
89 | AkkaInstrumentation.defineActorGroup(s"shardRegion/$typeName", Filter.fromGlob(entitiesPath))
90 | }
91 | }
92 |
93 | object InitializeShardAdvice {
94 |
95 | @Advice.OnMethodExit
96 | def exit(@Advice.This shard: Actor with HasShardingInstruments with HasShardCounters, @Advice.Argument(0) typeName: String,
97 | @Advice.Argument(1) shardID: String): Unit = {
98 |
99 | val shardingInstruments = new ShardingInstruments(shard.context.system.name, typeName)
100 | shard.setShardingInstruments(shardingInstruments)
101 | shard.setCounters(
102 | shardingInstruments.hostedEntitiesPerShardCounter(shardID),
103 | shardingInstruments.processedMessagesPerShardCounter(shardID)
104 | )
105 | }
106 | }
107 |
108 | object DeliverMessageOnShardRegion {
109 |
110 | @Advice.OnMethodEnter
111 | def enter(@Advice.This region: HasShardingInstruments, @Advice.Argument(0) message: Any): Unit = {
112 | // NOTE: The "deliverMessage" method also handles the "RestartShard" message, which is not an user-facing message
113 | // but it should not happen so often so we wont do any additional matching on it to filter it out of the
114 | // metric.
115 | region.shardingInstruments.processedMessages.increment()
116 | }
117 |
118 | }
119 |
120 | object RegionPostStopAdvice {
121 |
122 | @Advice.OnMethodExit
123 | def enter(@Advice.This shard: HasShardingInstruments): Unit =
124 | shard.shardingInstruments.remove()
125 | }
126 |
127 |
128 | object ShardOnLeaseAcquiredAdvice {
129 |
130 | @Advice.OnMethodExit
131 | def enter(@Advice.This shard: HasShardingInstruments): Unit =
132 | shard.shardingInstruments.hostedShards.increment()
133 | }
134 |
135 | object ShardPostStopStoppedAdvice {
136 |
137 | @Advice.OnMethodExit
138 | def enter(@Advice.This shard: HasShardingInstruments): Unit =
139 | shard.shardingInstruments.hostedShards.decrement()
140 | }
141 |
142 | object ShardGetOrCreateEntityAdvice {
143 |
144 | @Advice.OnMethodEnter
145 | def enter(@Advice.This shard: Actor with HasShardingInstruments with HasShardCounters, @Advice.Argument(0) entityID: String): Unit = {
146 | if(shard.context.child(entityID).isEmpty) {
147 | // The entity is not created just yet, but we know that it will be created right after this.
148 | shard.shardingInstruments.hostedEntities.increment()
149 | shard.hostedEntitiesCounter.incrementAndGet()
150 | }
151 | }
152 | }
153 |
154 | object ShardEntityTerminatedAdvice {
155 |
156 | @Advice.OnMethodEnter
157 | def enter(@Advice.This shard: Actor with HasShardingInstruments with HasShardCounters): Unit = {
158 | shard.shardingInstruments.hostedEntities.decrement()
159 | shard.hostedEntitiesCounter.decrementAndGet()
160 | }
161 | }
162 |
163 | object ShardDeliverMessageAdvice {
164 |
165 | @Advice.OnMethodEnter
166 | def enter(@Advice.This shard: Actor with HasShardingInstruments with HasShardCounters): Unit =
167 | shard.processedMessagesCounter.incrementAndGet()
168 |
169 | }
170 |
--------------------------------------------------------------------------------
/instrumentation/common/src/main/scala/kamon/instrumentation/akka/AkkaMetrics.scala:
--------------------------------------------------------------------------------
1 | package kamon.instrumentation.akka
2 |
3 | import kamon.Kamon
4 | import kamon.metric.InstrumentGroup
5 | import kamon.tag.TagSet
6 |
7 | import scala.collection.concurrent.TrieMap
8 |
9 | object AkkaMetrics {
10 |
11 | private val _groupInstrumentsCache = TrieMap.empty[String, ActorGroupInstruments]
12 | private val _systemInstrumentsCache = TrieMap.empty[String, ActorSystemInstruments]
13 |
14 | /**
15 | * Actor Metrics
16 | */
17 |
18 | val ActorTimeInMailbox = Kamon.timer (
19 | name = "akka.actor.time-in-mailbox",
20 | description = "Tracks the time since the instant a message is enqueued in an Actor's mailbox until it is dequeued for processing"
21 | )
22 |
23 | val ActorProcessingTime = Kamon.timer (
24 | name = "akka.actor.processing-time",
25 | description = "Tracks the time taken for the actor to process the receive function"
26 | )
27 |
28 | val ActorMailboxSize = Kamon.rangeSampler(
29 | name = "akka.actor.mailbox-size",
30 | description = "Tracks the behavior of an Actor's mailbox size"
31 | )
32 |
33 | val ActorErrors = Kamon.counter (
34 | name = "akka.actor.errors",
35 | description = "Counts the number of processing errors experienced by an Actor"
36 | )
37 |
38 | def forActor(path: String, system: String, dispatcher: String, actorClass: String): ActorInstruments =
39 | new ActorInstruments(TagSet.builder()
40 | .add("path", path)
41 | .add("system", system)
42 | .add("dispatcher", dispatcher)
43 | .add("class", actorClass)
44 | .build())
45 |
46 | class ActorInstruments(tags: TagSet) extends InstrumentGroup(tags) {
47 | val timeInMailbox = register(ActorTimeInMailbox)
48 | val processingTime = register(ActorProcessingTime)
49 | val mailboxSize = register(ActorMailboxSize)
50 | val errors = register(ActorErrors)
51 | }
52 |
53 |
54 | /**
55 | * Router Metrics
56 | */
57 |
58 | val RouterRoutingTime = Kamon.timer (
59 | name = "akka.router.routing-time",
60 | description = "Tracks the time taken by a router to process its routing logic"
61 | )
62 |
63 | val RouterTimeInMailbox = Kamon.timer (
64 | name = "akka.router.time-in-mailbox",
65 | description = "Tracks the time since the instant a message is enqueued in a routee's mailbox until it is dequeued for processing"
66 | )
67 |
68 | val RouterProcessingTime = Kamon.timer (
69 | name = "akka.router.processing-time",
70 | description = "Tracks the time taken for a routee to process the receive function"
71 | )
72 |
73 | val RouterPendingMessages = Kamon.rangeSampler (
74 | name = "akka.router.pending-messages",
75 | description = "Tracks the number of messages waiting to be processed across all routees"
76 | )
77 |
78 | val RouterMembers = Kamon.rangeSampler (
79 | name = "akka.router.members",
80 | description = "Tracks the number of routees belonging to a router"
81 | )
82 |
83 | val RouterErrors = Kamon.counter (
84 | name = "akka.router.errors",
85 | description = "Counts the number of processing errors experienced by the routees of a router"
86 | )
87 |
88 | def forRouter(path: String, system: String, dispatcher: String, routerClass: String, routeeClass: String): RouterInstruments =
89 | new RouterInstruments(TagSet.builder()
90 | .add("path", path)
91 | .add("system", system)
92 | .add("dispatcher", dispatcher)
93 | .add("routerClass", routerClass)
94 | .add("routeeClass", routeeClass)
95 | .build())
96 |
97 | class RouterInstruments(tags: TagSet) extends InstrumentGroup(tags) {
98 | val routingTime = register(RouterRoutingTime)
99 | val timeInMailbox = register(RouterTimeInMailbox)
100 | val processingTime = register(RouterProcessingTime)
101 | val pendingMessages = register(RouterPendingMessages)
102 | val members = register(RouterMembers)
103 | val errors = register(RouterErrors)
104 | }
105 |
106 |
107 | /**
108 | * Actor Group Metrics
109 | */
110 |
111 | val GroupTimeInMailbox = Kamon.timer (
112 | name = "akka.group.time-in-mailbox",
113 | description = "Tracks the time since the instant a message is enqueued in a member's mailbox until it is dequeued for processing"
114 | )
115 |
116 | val GroupProcessingTime = Kamon.timer (
117 | name = "akka.group.processing-time",
118 | description = "Tracks the time taken for a member actor to process the receive function"
119 | )
120 |
121 | val GroupPendingMessages = Kamon.rangeSampler (
122 | name = "akka.group.pending-messages",
123 | description = "Tracks the number of messages waiting to be processed across all members"
124 | )
125 |
126 | val GroupMembers = Kamon.rangeSampler (
127 | name = "akka.group.members",
128 | description = "Tracks the number of routees belonging to a group"
129 | )
130 |
131 | val GroupErrors = Kamon.counter (
132 | name = "akka.group.errors",
133 | description = "Counts the number of processing errors experienced by the members of a group"
134 | )
135 |
136 | def forGroup(group: String, system: String): ActorGroupInstruments =
137 | _groupInstrumentsCache.getOrElseUpdate(system + "/" + group, {
138 | val tags = TagSet.builder()
139 | .add("group", group)
140 | .add("system", system)
141 |
142 | new ActorGroupInstruments(tags.build())
143 | })
144 |
145 |
146 | case class ActorGroupInstruments(tags: TagSet) extends InstrumentGroup(tags) {
147 | val timeInMailbox = register(GroupTimeInMailbox)
148 | val processingTime = register(GroupProcessingTime)
149 | val pendingMessages = register(GroupPendingMessages)
150 | val members = register(GroupMembers)
151 | val errors = register(GroupErrors)
152 | }
153 |
154 |
155 | /**
156 | * Actor System Metrics
157 | */
158 |
159 | val SystemDeadLetters = Kamon.counter (
160 | name = "akka.system.dead-letters",
161 | description = "Counts the number of dead letters in an Actor System"
162 | )
163 |
164 | val SystemUnhandledMessages = Kamon.counter (
165 | name = "akka.system.unhandled-messages",
166 | description = "Counts the number of unhandled messages in an Actor System"
167 | )
168 |
169 | val SystemProcessedMessages = Kamon.counter (
170 | name = "akka.system.processed-messages",
171 | description = "Counts the number of processed messages in an Actor System"
172 | )
173 |
174 | val SystemActiveActors = Kamon.rangeSampler (
175 | name = "akka.system.active-actors",
176 | description = "Tracks the number of active Actors in an Actor System"
177 | )
178 |
179 | def forSystem(name: String): ActorSystemInstruments =
180 | _systemInstrumentsCache.atomicGetOrElseUpdate(name, new ActorSystemInstruments(TagSet.of("system", name)))
181 |
182 | class ActorSystemInstruments(tags: TagSet) extends InstrumentGroup(tags) {
183 | val deadLetters = register(SystemDeadLetters)
184 | val unhandledMessages = register(SystemUnhandledMessages)
185 | val processedMessagesByTracked = register(SystemProcessedMessages, "tracked", true)
186 | val processedMessagesByNonTracked = register(SystemProcessedMessages, "tracked", false)
187 | val activeActors = register(SystemActiveActors)
188 | }
189 | }
190 |
--------------------------------------------------------------------------------
/instrumentation/akka-2.6/src/main/scala/kamon/instrumentation/akka/instrumentations/akka_26/remote/RemotingInstrumentation.scala:
--------------------------------------------------------------------------------
1 | package kamon.instrumentation.akka.instrumentations.akka_26.remote
2 |
3 | import akka.actor.ActorSystem
4 | import akka.remote.kamon.instrumentation.akka.instrumentations.akka_26.remote.{CaptureContextOnInboundEnvelope, DeserializeForArteryAdvice, SerializeForArteryAdvice}
5 | import akka.kamon.instrumentation.akka.instrumentations.akka_26.remote.internal.{AkkaPduProtobufCodecConstructMessageMethodInterceptor, AkkaPduProtobufCodecDecodeMessage}
6 | import kamon.Kamon
7 | import kamon.context.Storage
8 | import kamon.context.Storage.Scope
9 | import kamon.instrumentation.akka.AkkaRemoteInstrumentation
10 | import kamon.instrumentation.akka.AkkaRemoteMetrics.SerializationInstruments
11 | import kamon.instrumentation.akka.instrumentations.{AkkaPrivateAccess, VersionFiltering}
12 | import kamon.instrumentation.context.{CaptureCurrentContextOnExit, HasContext}
13 | import kanela.agent.api.instrumentation.InstrumentationBuilder
14 | import kanela.agent.libs.net.bytebuddy.asm.Advice
15 |
16 |
17 | class RemotingInstrumentation extends InstrumentationBuilder with VersionFiltering {
18 |
19 | onAkka("2.6") {
20 |
21 | /**
22 | * Send messages might be buffered if they reach the EndpointWriter before it has been initialized and the current
23 | * Context might be lost after the buffering, so we make sure we capture the context when the Send command was
24 | * created and then apply it during the EndpointWrite.writeSend method execution (see bellow).
25 | */
26 | onType("akka.remote.EndpointManager$Send")
27 | .mixin(classOf[HasContext.Mixin])
28 | .advise(isConstructor, CaptureCurrentContextOnExit)
29 |
30 | onType("akka.remote.EndpointWriter")
31 | .advise(method("writeSend"), WriteSendWithContext)
32 |
33 | /**
34 | * Reads and writes the Akka PDU using a modified version of the Protobuf that has an extra field for a Context
35 | * instance.
36 | */
37 | onType("akka.remote.transport.AkkaPduProtobufCodec$")
38 | .intercept(method("constructMessage"), new AkkaPduProtobufCodecConstructMessageMethodInterceptor())
39 | .advise(method("decodeMessage"), classOf[AkkaPduProtobufCodecDecodeMessage])
40 |
41 | /**
42 | * Mixin Serialization Instruments to the Actor System and use them to record the serialization and deserialization
43 | * time metrics.
44 | */
45 | onType("akka.actor.ActorSystemImpl")
46 | .mixin(classOf[HasSerializationInstruments.Mixin])
47 | .advise(isConstructor, InitializeActorSystemAdvice)
48 |
49 | onType("akka.remote.MessageSerializer$")
50 | .advise(method("serialize"), MeasureSerializationTime)
51 | .advise(method("deserialize"), MeasureDeserializationTime)
52 |
53 |
54 | /**
55 | * Artery
56 | */
57 | onType("akka.remote.artery.ReusableOutboundEnvelope")
58 | .mixin(classOf[HasContext.Mixin])
59 | .advise(method("copy"), CopyContextOnReusableEnvelope)
60 |
61 | onType("akka.remote.artery.Association")
62 | .advise(method("createOutboundEnvelope$1"), CaptureCurrentContextOnReusableEnvelope)
63 |
64 | onType("akka.remote.MessageSerializer$")
65 | .advise(method("serializeForArtery"), classOf[SerializeForArteryAdvice])
66 | .advise(method("deserializeForArtery"), classOf[DeserializeForArteryAdvice])
67 |
68 | onType("akka.remote.artery.ReusableInboundEnvelope")
69 | .mixin(classOf[HasContext.Mixin])
70 | .advise(method("withMessage"), classOf[CaptureContextOnInboundEnvelope])
71 | .advise(method("copyForLane"), CopyContextOnReusableEnvelope)
72 |
73 | onType("akka.remote.artery.MessageDispatcher")
74 | .advise(method("dispatch"), ArteryMessageDispatcherAdvice)
75 | }
76 |
77 | }
78 |
79 | object ArteryMessageDispatcherAdvice {
80 |
81 | @Advice.OnMethodEnter
82 | def enter(@Advice.Argument(0) envelope: Any): Storage.Scope =
83 | Kamon.storeContext(envelope.asInstanceOf[HasContext].context)
84 |
85 | @Advice.OnMethodExit
86 | def exit(@Advice.Enter scope: Storage.Scope): Unit =
87 | scope.close()
88 | }
89 |
90 | object CopyContextOnReusableEnvelope {
91 |
92 | @Advice.OnMethodExit
93 | def exit(@Advice.This oldEnvelope: Any, @Advice.Return newEnvelope: Any): Unit =
94 | newEnvelope.asInstanceOf[HasContext].setContext(oldEnvelope.asInstanceOf[HasContext].context)
95 | }
96 |
97 | object CaptureCurrentContextOnReusableEnvelope {
98 |
99 | @Advice.OnMethodExit
100 | def exit(@Advice.Return envelope: Any): Unit = {
101 | envelope.asInstanceOf[HasContext].setContext(Kamon.currentContext())
102 | }
103 | }
104 |
105 | object WriteSendWithContext {
106 |
107 | @Advice.OnMethodEnter
108 | def enter(@Advice.Argument(0) send: Any): Scope = {
109 | Kamon.storeContext(send.asInstanceOf[HasContext].context)
110 | }
111 |
112 | @Advice.OnMethodExit
113 | def exit(@Advice.Enter scope: Scope): Unit = {
114 | scope.asInstanceOf[Scope].close()
115 | }
116 | }
117 |
118 | trait HasSerializationInstruments {
119 | def serializationInstruments: SerializationInstruments
120 | def setSerializationInstruments(instruments: SerializationInstruments): Unit
121 | }
122 |
123 | object HasSerializationInstruments {
124 |
125 | class Mixin(var serializationInstruments: SerializationInstruments) extends HasSerializationInstruments {
126 | override def setSerializationInstruments(instruments: SerializationInstruments): Unit =
127 | serializationInstruments = instruments
128 | }
129 | }
130 |
131 | object InitializeActorSystemAdvice {
132 |
133 | @Advice.OnMethodExit
134 | def exit(@Advice.This system: ActorSystem with HasSerializationInstruments): Unit =
135 | system.setSerializationInstruments(new SerializationInstruments(system.name))
136 |
137 | }
138 |
139 | object MeasureSerializationTime {
140 |
141 | @Advice.OnMethodEnter
142 | def enter(): Long = {
143 | if(AkkaRemoteInstrumentation.settings().trackSerializationMetrics) System.nanoTime() else 0L
144 | }
145 |
146 | @Advice.OnMethodExit
147 | def exit(@Advice.Argument(0) system: AnyRef, @Advice.Enter startNanoTime: Long): Unit = {
148 | if(startNanoTime != 0L) {
149 | system.asInstanceOf[HasSerializationInstruments]
150 | .serializationInstruments
151 | .serializationTime
152 | .record(System.nanoTime() - startNanoTime)
153 | }
154 | }
155 | }
156 |
157 | object MeasureDeserializationTime {
158 |
159 | @Advice.OnMethodEnter
160 | def enter(): Long = {
161 | if(AkkaRemoteInstrumentation.settings().trackSerializationMetrics) System.nanoTime() else 0L
162 | }
163 |
164 | @Advice.OnMethodExit
165 | def exit(@Advice.Argument(0) system: AnyRef, @Advice.Enter startNanoTime: Long, @Advice.Return msg: Any): Unit = {
166 |
167 | if(AkkaPrivateAccess.isSystemMessage(msg)) {
168 | msg match {
169 | case hc: HasContext if hc.context == null =>
170 | hc.setContext(Kamon.currentContext())
171 | case _ =>
172 | }
173 | }
174 |
175 | if(startNanoTime != 0L) {
176 | system.asInstanceOf[HasSerializationInstruments]
177 | .serializationInstruments
178 | .deserializationTime
179 | .record(System.nanoTime() - startNanoTime)
180 | }
181 | }
182 | }
183 |
--------------------------------------------------------------------------------
/test/akka-2.4/src/test/scala/kamon/instrumentation/akka/remote/RemotingInstrumentationSpec.scala:
--------------------------------------------------------------------------------
1 | package kamon.instrumentation.akka.remote
2 |
3 | import akka.actor.SupervisorStrategy.Resume
4 | import akka.actor._
5 | import akka.pattern.{ask, pipe}
6 | import akka.routing.RoundRobinGroup
7 | import akka.testkit.{ImplicitSender, TestKitBase}
8 | import akka.util.Timeout
9 | import com.typesafe.config.ConfigFactory
10 | import kamon.Kamon
11 | import kamon.context.Context
12 | import kamon.instrumentation.akka.AkkaRemoteMetrics.{DeserializationTime, SerializationTime}
13 | import kamon.instrumentation.akka.{AkkaRemoteMetrics, ContextEchoActor}
14 | import kamon.tag.Lookups._
15 | import kamon.tag.TagSet
16 | import kamon.testkit.{InstrumentInspection, MetricInspection}
17 | import org.scalatest.Inspectors._
18 | import org.scalatest.{Matchers, WordSpecLike}
19 |
20 | import scala.concurrent.duration._
21 | import scala.util.control.NonFatal
22 |
23 | class RemotingInstrumentationSpec extends TestKitBase with WordSpecLike with Matchers with ImplicitSender
24 | with MetricInspection.Syntax with InstrumentInspection.Syntax {
25 |
26 | implicit lazy val system: ActorSystem = {
27 | ActorSystem("remoting-spec-local-system", ConfigFactory.parseString(
28 | """
29 | |akka {
30 | | actor {
31 | | provider = "akka.remote.RemoteActorRefProvider"
32 | | }
33 | | remote {
34 | | enabled-transports = ["akka.remote.netty.tcp"]
35 | | netty.tcp {
36 | | hostname = "127.0.0.1"
37 | | port = 2552
38 | | }
39 | | }
40 | |}
41 | """.stripMargin))
42 | }
43 |
44 | val remoteSystem: ActorSystem = ActorSystem("remoting-spec-remote-system", ConfigFactory.parseString(
45 | """
46 | |akka {
47 | | actor {
48 | | provider = "akka.remote.RemoteActorRefProvider"
49 | | }
50 | | remote {
51 | | enabled-transports = ["akka.remote.netty.tcp"]
52 | | netty.tcp {
53 | | hostname = "127.0.0.1"
54 | | port = 2553
55 | | }
56 | | }
57 | |}
58 | """.stripMargin))
59 |
60 | val RemoteSystemAddress = AddressFromURIString("akka.tcp://remoting-spec-remote-system@127.0.0.1:2553")
61 |
62 | def contextWithBroadcast(name: String): Context =
63 | Context.Empty.withTag(
64 | ContextEchoActor.EchoTag,
65 | name
66 | )
67 |
68 | "The Akka Remote instrumentation" should {
69 | "propagate the current Context when creating a new remote actor" in {
70 | val a = Kamon.runWithContext(contextWithBroadcast("deploy-remote-actor-1")) {
71 | system.actorOf(ContextEchoActor.remoteProps(Some(testActor), RemoteSystemAddress), "remote-deploy-fixture")
72 | }
73 |
74 | expectMsg(10 seconds, "name=deploy-remote-actor-1")
75 | }
76 |
77 |
78 | "propagate the Context when sending a message to a remotely deployed actor" in {
79 | val remoteRef = system.actorOf(ContextEchoActor.remoteProps(None, RemoteSystemAddress), "remote-message-fixture")
80 |
81 | Kamon.runWithContext(contextWithBroadcast("message-remote-actor-1")) {
82 | remoteRef ! "reply-trace-token"
83 | }
84 | expectMsg("name=message-remote-actor-1")
85 | }
86 |
87 |
88 | "propagate the current Context when pipe or ask a message to a remotely deployed actor" in {
89 | implicit val ec = system.dispatcher
90 | implicit val askTimeout = Timeout(10 seconds)
91 | val remoteRef = system.actorOf(ContextEchoActor.remoteProps(None, RemoteSystemAddress), "remote-ask-and-pipe-fixture")
92 |
93 | Kamon.runWithContext(contextWithBroadcast("ask-and-pipe-remote-actor-1")) {
94 | (remoteRef ? "reply-trace-token") pipeTo testActor
95 | }
96 |
97 | expectMsg("name=ask-and-pipe-remote-actor-1")
98 | }
99 |
100 |
101 | "propagate the current Context when sending a message to an ActorSelection" in {
102 | remoteSystem.actorOf(ContextEchoActor.props(None), "actor-selection-target-a")
103 | remoteSystem.actorOf(ContextEchoActor.props(None), "actor-selection-target-b")
104 | val selection = system.actorSelection(RemoteSystemAddress + "/user/actor-selection-target-*")
105 |
106 | Kamon.runWithContext(contextWithBroadcast("message-remote-actor-selection-1")) {
107 | selection ! "reply-trace-token"
108 | }
109 |
110 | // one for each selected actor
111 | expectMsg("name=message-remote-actor-selection-1")
112 | expectMsg("name=message-remote-actor-selection-1")
113 | }
114 |
115 | "propagate the current Context when sending messages to remote routees of a router" in {
116 | remoteSystem.actorOf(ContextEchoActor.props(None), "router-target-a")
117 | remoteSystem.actorOf(ContextEchoActor.props(None), "router-target-b")
118 | val router = system.actorOf(RoundRobinGroup(List(
119 | RemoteSystemAddress + "/user/router-target-a",
120 | RemoteSystemAddress + "/user/router-target-b"
121 | )).props(), "router")
122 |
123 | Kamon.runWithContext(contextWithBroadcast("remote-routee-1")) {
124 | router ! "reply-trace-token"
125 | }
126 |
127 | expectMsg("name=remote-routee-1")
128 | }
129 |
130 | "propagate the current Context when a remotely supervised child fails" in {
131 | val supervisor = system.actorOf(Props(new SupervisorOfRemote(testActor, RemoteSystemAddress)),"SUPERVISOR")
132 |
133 | Kamon.runWithContext(contextWithBroadcast("remote-supervision-1")) {
134 | supervisor ! "fail"
135 | }
136 |
137 | expectMsg(2 minutes,"name=remote-supervision-1")
138 | }
139 |
140 | "record in/out message counts and sizes for both sending and receiving side" in {
141 | val (out, in) = (
142 | AkkaRemoteMetrics.OutboundMessageSize.withTags(TagSet.of("system", system.name)).distribution(false),
143 | AkkaRemoteMetrics.OutboundMessageSize.withTags(TagSet.of("system", system.name)).distribution(false)
144 | )
145 |
146 | assert(out.max > 0)
147 | assert(in.max > 0)
148 | assert(out.count > 0)
149 | assert(in.count > 0)
150 | }
151 |
152 | "record de/serialization times for messages" in {
153 | val systems = Seq(system.name, remoteSystem.name)
154 | val serializationTimes = systems.map(s => SerializationTime.withTags(TagSet.of("system", s)).distribution().count)
155 | val deserializationTimes = systems.map(s => DeserializationTime.withTags(TagSet.of("system", s)).distribution().count)
156 |
157 | forAll(serializationTimes ++ deserializationTimes) { count => assert(count > 0) }
158 | }
159 | }
160 | }
161 |
162 | class SupervisorOfRemote(echoListener: ActorRef, remoteAddress: Address) extends Actor {
163 | val supervisedChild = context.actorOf(ContextEchoActor.remoteProps(None, remoteAddress), "remotely-supervised-child")
164 |
165 | def receive = {
166 | case "fail" => supervisedChild ! "die"
167 | }
168 |
169 | override def supervisorStrategy: SupervisorStrategy = OneForOneStrategy() {
170 | case NonFatal(_) =>
171 | echoListener ! currentEchoMessage
172 | Resume
173 | case _ => Resume
174 | }
175 |
176 | def currentEchoMessage: String = {
177 | val ctx = Kamon.currentContext()
178 | val name = ctx.getTag(option(ContextEchoActor.EchoTag)).getOrElse("")
179 | s"name=$name"
180 | }
181 | }
--------------------------------------------------------------------------------
/test/akka-2.5/src/test/scala/kamon/instrumentation/akka/remote/RemotingInstrumentationSpec.scala:
--------------------------------------------------------------------------------
1 | package kamon.instrumentation.akka.remote
2 |
3 | import akka.actor.SupervisorStrategy.Resume
4 | import akka.actor._
5 | import akka.pattern.{ask, pipe}
6 | import akka.routing.RoundRobinGroup
7 | import akka.testkit.{ImplicitSender, TestKitBase}
8 | import akka.util.Timeout
9 | import com.typesafe.config.ConfigFactory
10 | import kamon.Kamon
11 | import kamon.context.Context
12 | import kamon.instrumentation.akka.AkkaRemoteMetrics.{DeserializationTime, SerializationTime}
13 | import kamon.instrumentation.akka.{AkkaRemoteMetrics, ContextEchoActor}
14 | import kamon.tag.Lookups._
15 | import kamon.tag.TagSet
16 | import kamon.testkit.{InstrumentInspection, MetricInspection}
17 | import org.scalatest.Inspectors._
18 | import org.scalatest.{Matchers, WordSpecLike}
19 |
20 | import scala.concurrent.duration._
21 | import scala.util.control.NonFatal
22 |
23 | class RemotingInstrumentationSpec extends TestKitBase with WordSpecLike with Matchers with ImplicitSender
24 | with MetricInspection.Syntax with InstrumentInspection.Syntax {
25 |
26 | implicit lazy val system: ActorSystem = {
27 | ActorSystem("remoting-spec-local-system", ConfigFactory.parseString(
28 | """
29 | |akka {
30 | | actor {
31 | | provider = "akka.remote.RemoteActorRefProvider"
32 | | }
33 | | remote {
34 | | enabled-transports = ["akka.remote.netty.tcp"]
35 | | netty.tcp {
36 | | hostname = "127.0.0.1"
37 | | port = 2552
38 | | }
39 | | }
40 | |}
41 | """.stripMargin))
42 | }
43 |
44 | val remoteSystem: ActorSystem = ActorSystem("remoting-spec-remote-system", ConfigFactory.parseString(
45 | """
46 | |akka {
47 | | actor {
48 | | provider = "akka.remote.RemoteActorRefProvider"
49 | | }
50 | | remote {
51 | | enabled-transports = ["akka.remote.netty.tcp"]
52 | | netty.tcp {
53 | | hostname = "127.0.0.1"
54 | | port = 2553
55 | | }
56 | | }
57 | |}
58 | """.stripMargin))
59 |
60 | val RemoteSystemAddress = AddressFromURIString("akka.tcp://remoting-spec-remote-system@127.0.0.1:2553")
61 |
62 | def contextWithBroadcast(name: String): Context =
63 | Context.Empty.withTag(
64 | ContextEchoActor.EchoTag,
65 | name
66 | )
67 |
68 | "The Akka Remote instrumentation" should {
69 | "propagate the current Context when creating a new remote actor" in {
70 | val a = Kamon.runWithContext(contextWithBroadcast("deploy-remote-actor-1")) {
71 | system.actorOf(ContextEchoActor.remoteProps(Some(testActor), RemoteSystemAddress), "remote-deploy-fixture")
72 | }
73 |
74 | expectMsg(10 seconds, "name=deploy-remote-actor-1")
75 | }
76 |
77 |
78 | "propagate the Context when sending a message to a remotely deployed actor" in {
79 | val remoteRef = system.actorOf(ContextEchoActor.remoteProps(None, RemoteSystemAddress), "remote-message-fixture")
80 |
81 | Kamon.runWithContext(contextWithBroadcast("message-remote-actor-1")) {
82 | remoteRef ! "reply-trace-token"
83 | }
84 | expectMsg("name=message-remote-actor-1")
85 | }
86 |
87 |
88 | "propagate the current Context when pipe or ask a message to a remotely deployed actor" in {
89 | implicit val ec = system.dispatcher
90 | implicit val askTimeout = Timeout(10 seconds)
91 | val remoteRef = system.actorOf(ContextEchoActor.remoteProps(None, RemoteSystemAddress), "remote-ask-and-pipe-fixture")
92 |
93 | Kamon.runWithContext(contextWithBroadcast("ask-and-pipe-remote-actor-1")) {
94 | (remoteRef ? "reply-trace-token") pipeTo testActor
95 | }
96 |
97 | expectMsg("name=ask-and-pipe-remote-actor-1")
98 | }
99 |
100 |
101 | "propagate the current Context when sending a message to an ActorSelection" in {
102 | remoteSystem.actorOf(ContextEchoActor.props(None), "actor-selection-target-a")
103 | remoteSystem.actorOf(ContextEchoActor.props(None), "actor-selection-target-b")
104 | val selection = system.actorSelection(RemoteSystemAddress + "/user/actor-selection-target-*")
105 |
106 | Kamon.runWithContext(contextWithBroadcast("message-remote-actor-selection-1")) {
107 | selection ! "reply-trace-token"
108 | }
109 |
110 | // one for each selected actor
111 | expectMsg("name=message-remote-actor-selection-1")
112 | expectMsg("name=message-remote-actor-selection-1")
113 | }
114 |
115 | "propagate the current Context when sending messages to remote routees of a router" in {
116 | remoteSystem.actorOf(ContextEchoActor.props(None), "router-target-a")
117 | remoteSystem.actorOf(ContextEchoActor.props(None), "router-target-b")
118 | val router = system.actorOf(RoundRobinGroup(List(
119 | RemoteSystemAddress + "/user/router-target-a",
120 | RemoteSystemAddress + "/user/router-target-b"
121 | )).props(), "router")
122 |
123 | Kamon.runWithContext(contextWithBroadcast("remote-routee-1")) {
124 | router ! "reply-trace-token"
125 | }
126 |
127 | expectMsg("name=remote-routee-1")
128 | }
129 |
130 | "propagate the current Context when a remotely supervised child fails" in {
131 | val supervisor = system.actorOf(Props(new SupervisorOfRemote(testActor, RemoteSystemAddress)),"SUPERVISOR")
132 |
133 | Kamon.runWithContext(contextWithBroadcast("remote-supervision-1")) {
134 | supervisor ! "fail"
135 | }
136 |
137 | expectMsg(2 minutes,"name=remote-supervision-1")
138 | }
139 |
140 | "record in/out message counts and sizes for both sending and receiving side" in {
141 | val (out, in) = (
142 | AkkaRemoteMetrics.OutboundMessageSize.withTags(TagSet.of("system", system.name)).distribution(false),
143 | AkkaRemoteMetrics.OutboundMessageSize.withTags(TagSet.of("system", system.name)).distribution(false)
144 | )
145 |
146 | assert(out.max > 0)
147 | assert(in.max > 0)
148 | assert(out.count > 0)
149 | assert(in.count > 0)
150 | }
151 |
152 | "record de/serialization times for messages" in {
153 | val systems = Seq(system.name, remoteSystem.name)
154 | val serializationTimes = systems.map(s => SerializationTime.withTags(TagSet.of("system", s)).distribution().count)
155 | val deserializationTimes = systems.map(s => DeserializationTime.withTags(TagSet.of("system", s)).distribution().count)
156 |
157 | forAll(serializationTimes ++ deserializationTimes) { count => assert(count > 0) }
158 | }
159 | }
160 | }
161 |
162 | class SupervisorOfRemote(echoListener: ActorRef, remoteAddress: Address) extends Actor {
163 | val supervisedChild = context.actorOf(ContextEchoActor.remoteProps(None, remoteAddress), "remotely-supervised-child")
164 |
165 | def receive = {
166 | case "fail" => supervisedChild ! "die"
167 | }
168 |
169 | override def supervisorStrategy: SupervisorStrategy = OneForOneStrategy() {
170 | case NonFatal(_) =>
171 | echoListener ! currentEchoMessage
172 | Resume
173 | case _ => Resume
174 | }
175 |
176 | def currentEchoMessage: String = {
177 | val ctx = Kamon.currentContext()
178 | val name = ctx.getTag(option(ContextEchoActor.EchoTag)).getOrElse("")
179 | s"name=$name"
180 | }
181 | }
--------------------------------------------------------------------------------