├── 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 | } --------------------------------------------------------------------------------