├── project ├── build.properties └── plugins.sbt ├── version.sbt ├── .gitignore ├── .scala-steward.conf ├── Contributing.md ├── src ├── test │ ├── scala-2 │ │ └── io │ │ │ └── kontainers │ │ │ └── micrometer │ │ │ └── akka │ │ │ └── VersionUtil.scala │ ├── scala-3 │ │ └── io │ │ │ └── kontainers │ │ │ └── micrometer │ │ │ └── akka │ │ │ └── VersionUtil.scala │ ├── resources │ │ ├── logback-test.xml │ │ ├── application.conf │ │ └── application-alt.conf │ └── scala │ │ ├── io │ │ └── kontainers │ │ │ └── micrometer │ │ │ └── akka │ │ │ ├── BaseSpec.scala │ │ │ ├── ActorMetricsTestActor.scala │ │ │ ├── RouterMetricsTestActor.scala │ │ │ ├── MetricsConfigSpec.scala │ │ │ ├── ForkJoinPoolMetricsSpec.scala │ │ │ ├── impl │ │ │ ├── RegexPathFilterSpec.scala │ │ │ └── GlobPathFilterSpec.scala │ │ │ ├── DispatcherMetricsSpec.scala │ │ │ ├── ActorMetricsSpec.scala │ │ │ ├── ActorSystemMetricsSpec.scala │ │ │ ├── RouterMetricsSpec.scala │ │ │ └── ActorGroupMetricsSpec.scala │ │ └── akka │ │ └── monitor │ │ └── instrumentation │ │ └── EnvelopeSpec.scala └── main │ ├── scala │ ├── io │ │ └── kontainers │ │ │ └── micrometer │ │ │ └── akka │ │ │ ├── TimerWrapper.scala │ │ │ ├── GaugeWrapper.scala │ │ │ ├── Entity.scala │ │ │ ├── impl │ │ │ ├── DoubleFunction.scala │ │ │ └── EntityFilter.scala │ │ │ ├── ActorSystemMetrics.scala │ │ │ ├── package.scala │ │ │ ├── ActorMetrics.scala │ │ │ ├── RouterMetrics.scala │ │ │ ├── ActorGroupMetrics.scala │ │ │ ├── ThreadPoolMetrics.scala │ │ │ ├── MetricsConfig.scala │ │ │ └── AkkaMetricRegistry.scala │ └── akka │ │ └── monitor │ │ └── instrumentation │ │ ├── MetricsIntoActorCellsMixin.scala │ │ ├── ActorInstrumentationAware.scala │ │ ├── EnvelopeInstrumentation.scala │ │ ├── DeadLettersInstrumentation.scala │ │ ├── CellInfo.scala │ │ ├── RouterMonitor.scala │ │ ├── RouterInstrumentation.scala │ │ ├── ActorCellInstrumentation.scala │ │ ├── ActorMonitor.scala │ │ └── DispatcherInstrumentation.scala │ ├── resources │ ├── reference.conf │ └── META-INF │ │ └── aop.xml │ ├── scala-2 │ └── io │ │ └── kontainers │ │ └── micrometer │ │ └── akka │ │ └── ForkJoinPoolMetrics.scala │ └── scala-3 │ └── io │ └── kontainers │ └── micrometer │ └── akka │ └── ForkJoinPoolMetrics.scala ├── .travis.yml ├── README.md └── LICENSE /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.7.1 2 | -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | ThisBuild / version := "0.13.0-SNAPSHOT" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | prometheus-akka.iml 3 | .bsp/sbt.json 4 | -------------------------------------------------------------------------------- /.scala-steward.conf: -------------------------------------------------------------------------------- 1 | updates.ignore = [{ groupId = "com.typesafe.akka" }] -------------------------------------------------------------------------------- /Contributing.md: -------------------------------------------------------------------------------- 1 | Please feel free to submit issues or to create Pull Requests. 2 | -------------------------------------------------------------------------------- /src/test/scala-2/io/kontainers/micrometer/akka/VersionUtil.scala: -------------------------------------------------------------------------------- 1 | package io.kontainers.micrometer.akka 2 | 3 | object VersionUtil { 4 | def isScala3: Boolean = false 5 | } 6 | -------------------------------------------------------------------------------- /src/test/scala-3/io/kontainers/micrometer/akka/VersionUtil.scala: -------------------------------------------------------------------------------- 1 | package io.kontainers.micrometer.akka 2 | 3 | object VersionUtil { 4 | def isScala3: Boolean = true 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | jdk: 3 | - openjdk8 4 | - openjdk11 5 | - openjdk17 6 | scala: 7 | - 2.12.12 8 | - 2.13.7 9 | script: 10 | - sbt ++$TRAVIS_SCALA_VERSION coverage test coverageReport 11 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.lightbend.sbt" % "sbt-javaagent" % "0.1.6") 2 | addSbtPlugin("com.github.sbt" % "sbt-release" % "1.1.0") 3 | addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.1.2") 4 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.2") 5 | 6 | -------------------------------------------------------------------------------- /src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main/scala/io/kontainers/micrometer/akka/TimerWrapper.scala: -------------------------------------------------------------------------------- 1 | package io.kontainers.micrometer.akka 2 | 3 | import java.io.Closeable 4 | import java.util.concurrent.TimeUnit 5 | 6 | import io.micrometer.core.instrument.Timer 7 | 8 | case class TimerWrapper(timer: Timer) { 9 | 10 | class TimeObservation(timer: Timer, startTime: Long) extends Closeable { 11 | def close(): Unit = timer.record(System.nanoTime() - startTime, TimeUnit.NANOSECONDS) 12 | } 13 | 14 | def startTimer(): TimeObservation = new TimeObservation(timer, System.nanoTime()) 15 | } 16 | -------------------------------------------------------------------------------- /src/main/scala/io/kontainers/micrometer/akka/GaugeWrapper.scala: -------------------------------------------------------------------------------- 1 | package io.kontainers.micrometer.akka 2 | 3 | import java.util.concurrent.atomic.DoubleAdder 4 | 5 | import scala.collection.JavaConverters._ 6 | 7 | import io.kontainers.micrometer.akka.impl.DoubleFunction 8 | import io.micrometer.core.instrument.{MeterRegistry, Tag} 9 | 10 | case class GaugeWrapper(registry: MeterRegistry, name: String, tags: Iterable[Tag]) { 11 | private val adder = new DoubleAdder 12 | private val fn = new DoubleFunction[DoubleAdder](_.doubleValue) 13 | registry.gauge(name, tags.asJava, adder, fn) 14 | def decrement(): Unit = increment(-1.0) 15 | def increment(): Unit = increment(1.0) 16 | def increment(d: Double): Unit = adder.add(d) 17 | } 18 | -------------------------------------------------------------------------------- /src/main/resources/reference.conf: -------------------------------------------------------------------------------- 1 | # ======================================= # 2 | # Micrometer-Akka Reference Configuration # 3 | # ======================================= # 4 | 5 | micrometer.akka { 6 | histogram.buckets.enabled = true 7 | match.events = true 8 | # executor-service.style can be `internal` or `core` 9 | # metrics are presented in legacy style or registered using io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics 10 | executor-service.style = "internal" 11 | metric.filters { 12 | akka-actor { 13 | includes = [] 14 | excludes = [ "*/system/**", "*/user/IO-**" ] 15 | } 16 | 17 | akka-router { 18 | includes = [] 19 | excludes = [] 20 | } 21 | 22 | akka-dispatcher { 23 | includes = ["**"] 24 | excludes = [] 25 | } 26 | 27 | akka-actor-groups { 28 | //include empty actor-group to demonstrate the config 29 | empty { 30 | includes = [] 31 | excludes = ["**"] 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/scala/io/kontainers/micrometer/akka/Entity.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2017,2018 Workday, Inc. 4 | * Copyright © 2013-2017 the kamon project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 7 | * except in compliance with the License. You may obtain a copy of 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 distributed under the 12 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific language governing permissions 14 | * and limitations under the License. 15 | * ========================================================================================= 16 | */ 17 | package io.kontainers.micrometer.akka 18 | 19 | case class Entity(name: String, category: String) 20 | -------------------------------------------------------------------------------- /src/main/scala/io/kontainers/micrometer/akka/impl/DoubleFunction.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2017,2018 Workday, Inc. 4 | * Copyright © 2013-2017 the kamon project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 7 | * except in compliance with the License. You may obtain a copy of 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 distributed under the 12 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific language governing permissions 14 | * and limitations under the License. 15 | * ========================================================================================= 16 | */ 17 | package io.kontainers.micrometer.akka.impl 18 | 19 | import java.util.function.ToDoubleFunction 20 | 21 | private[akka] class DoubleFunction[T](fun: T => Double) extends ToDoubleFunction[T] { 22 | override def applyAsDouble(t: T): Double = fun(t) 23 | } 24 | -------------------------------------------------------------------------------- /src/main/scala/akka/monitor/instrumentation/MetricsIntoActorCellsMixin.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2017,2018 Workday, Inc. 4 | * Copyright © 2013-2017 the kamon project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 7 | * except in compliance with the License. You may obtain a copy of 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 distributed under the 12 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific language governing permissions 14 | * and limitations under the License. 15 | * ========================================================================================= 16 | */ 17 | package akka.monitor.instrumentation 18 | 19 | import org.aspectj.lang.annotation.{Aspect, DeclareMixin} 20 | 21 | @Aspect 22 | class MetricsIntoActorCellsMixin { 23 | 24 | @DeclareMixin("akka.actor.ActorCell") 25 | def mixinActorCellMetricsToActorCell: ActorInstrumentationAware = ActorInstrumentationAware() 26 | 27 | @DeclareMixin("akka.actor.UnstartedCell") 28 | def mixinActorCellMetricsToUnstartedActorCell: ActorInstrumentationAware = ActorInstrumentationAware() 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/scala/akka/monitor/instrumentation/ActorInstrumentationAware.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2017,2018 Workday, Inc. 4 | * Copyright © 2013-2017 the kamon project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 7 | * except in compliance with the License. You may obtain a copy of 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 distributed under the 12 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific language governing permissions 14 | * and limitations under the License. 15 | * ========================================================================================= 16 | */ 17 | package akka.monitor.instrumentation 18 | 19 | trait ActorInstrumentationAware { 20 | def actorInstrumentation: ActorMonitor 21 | def setActorInstrumentation(ai: ActorMonitor): Unit 22 | } 23 | 24 | object ActorInstrumentationAware { 25 | def apply(): ActorInstrumentationAware = new ActorInstrumentationAware { 26 | private var _ai: ActorMonitor = _ 27 | 28 | def setActorInstrumentation(ai: ActorMonitor): Unit = _ai = ai 29 | def actorInstrumentation: ActorMonitor = _ai 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/scala/io/kontainers/micrometer/akka/BaseSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2017,2018 Workday, Inc. 4 | * Copyright © 2013-2017 the kamon project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 7 | * except in compliance with the License. You may obtain a copy of 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 distributed under the 12 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific language governing permissions 14 | * and limitations under the License. 15 | * ========================================================================================= 16 | */ 17 | package io.kontainers.micrometer.akka 18 | 19 | import org.scalatest.BeforeAndAfterAll 20 | 21 | import akka.actor.ActorSystem 22 | import akka.testkit.TestKit 23 | import org.scalatest.matchers.should.Matchers 24 | import org.scalatest.wordspec.AnyWordSpecLike 25 | 26 | trait BaseSpec extends AnyWordSpecLike with Matchers with BeforeAndAfterAll 27 | 28 | abstract class TestKitBaseSpec(actorSystemName: String) extends TestKit(ActorSystem(actorSystemName)) with BaseSpec { 29 | override def afterAll(): Unit = { 30 | super.afterAll() 31 | TestKit.shutdownActorSystem(system) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/test/resources/application.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | loglevel = INFO 3 | loggers = [ "akka.event.slf4j.Slf4jLogger" ] 4 | logger-startup-timeout = 30s 5 | } 6 | 7 | micrometer.akka { 8 | metric.filters { 9 | akka-actor { 10 | includes = [ "**/user/tracked-**", "*/user/measuring-**", "*/user/stop-**" ] 11 | excludes = [ "*/system/**", "*/user/IO-**", "**/user/tracked-explicitly-excluded-**" ] 12 | } 13 | 14 | akka-router { 15 | includes = [ "**/user/tracked-**", "*/user/measuring-**", "*/user/stop-**" ] 16 | excludes = [ "**/user/tracked-explicitly-excluded-**" ] 17 | } 18 | 19 | akka-dispatcher { 20 | includes = [ "**" ] 21 | excludes = [ "**explicitly-excluded**" ] 22 | } 23 | 24 | akka-actor-groups { 25 | all { 26 | includes = [ "**" ] 27 | excludes = [ "*/system/**", "*/user/IO-**" ] 28 | } 29 | tracked { 30 | includes = [ "**/user/tracked-**" ] 31 | excludes = [ "*/system/**", "*/user/IO-**", "**/user/tracked-explicitly-excluded-**" ] 32 | } 33 | exclusive { 34 | includes = [ "**/MyActor**" ] 35 | excludes = [] 36 | } 37 | } 38 | } 39 | } 40 | 41 | explicitly-excluded { 42 | type = "Dispatcher" 43 | executor = "fork-join-executor" 44 | } 45 | 46 | tracked-fjp { 47 | type = "Dispatcher" 48 | executor = "fork-join-executor" 49 | 50 | fork-join-executor { 51 | parallelism-min = 8 52 | parallelism-factor = 100.0 53 | parallelism-max = 22 54 | } 55 | } 56 | 57 | tracked-tpe { 58 | type = "Dispatcher" 59 | executor = "thread-pool-executor" 60 | 61 | thread-pool-executor { 62 | core-pool-size-min = 7 63 | core-pool-size-factor = 100.0 64 | max-pool-size-factor = 100.0 65 | max-pool-size-max = 21 66 | core-pool-size-max = 21 67 | } 68 | } -------------------------------------------------------------------------------- /src/test/resources/application-alt.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | loglevel = INFO 3 | loggers = [ "akka.event.slf4j.Slf4jLogger" ] 4 | logger-startup-timeout = 30s 5 | } 6 | 7 | micrometer.akka { 8 | executor-service.style = "core" 9 | metric.filters { 10 | akka-actor { 11 | includes = [ "**/user/tracked-**", "*/user/measuring-**", "*/user/stop-**" ] 12 | excludes = [ "*/system/**", "*/user/IO-**", "**/user/tracked-explicitly-excluded-**" ] 13 | } 14 | 15 | akka-router { 16 | includes = [ "**/user/tracked-**", "*/user/measuring-**", "*/user/stop-**" ] 17 | excludes = [ "**/user/tracked-explicitly-excluded-**" ] 18 | } 19 | 20 | akka-dispatcher { 21 | includes = [ "**" ] 22 | excludes = [ "**explicitly-excluded**" ] 23 | } 24 | 25 | akka-actor-groups { 26 | all { 27 | includes = [ "**" ] 28 | excludes = [ "*/system/**", "*/user/IO-**" ] 29 | } 30 | tracked { 31 | includes = [ "**/user/tracked-**" ] 32 | excludes = [ "*/system/**", "*/user/IO-**", "**/user/tracked-explicitly-excluded-**" ] 33 | } 34 | exclusive { 35 | includes = [ "**/MyActor**" ] 36 | excludes = [] 37 | } 38 | } 39 | } 40 | } 41 | 42 | explicitly-excluded { 43 | type = "Dispatcher" 44 | executor = "fork-join-executor" 45 | } 46 | 47 | tracked-fjp { 48 | type = "Dispatcher" 49 | executor = "fork-join-executor" 50 | 51 | fork-join-executor { 52 | parallelism-min = 8 53 | parallelism-factor = 100.0 54 | parallelism-max = 22 55 | } 56 | } 57 | 58 | tracked-tpe { 59 | type = "Dispatcher" 60 | executor = "thread-pool-executor" 61 | 62 | thread-pool-executor { 63 | core-pool-size-min = 7 64 | core-pool-size-factor = 100.0 65 | max-pool-size-factor = 100.0 66 | max-pool-size-max = 21 67 | core-pool-size-max = 21 68 | } 69 | } -------------------------------------------------------------------------------- /src/main/scala/io/kontainers/micrometer/akka/ActorSystemMetrics.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2017,2018 Workday, Inc. 4 | * Copyright © 2013-2017 the kamon project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 7 | * except in compliance with the License. You may obtain a copy of 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 distributed under the 12 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific language governing permissions 14 | * and limitations under the License. 15 | * ========================================================================================= 16 | */ 17 | package io.kontainers.micrometer.akka 18 | 19 | import io.micrometer.core.instrument.{Counter, ImmutableTag, Tag} 20 | 21 | object ActorSystemMetrics { 22 | 23 | val ActorSystem = "actorSystem" 24 | 25 | private[akka] val ActorCountMetricName = "akka_system_actor_count" 26 | private[akka] val DeadLetterCountMetricName = "akka_system_dead_letter_count" 27 | private[akka] val UnhandledMessageCountMetricName = "akka_system_unhandled_message_count" 28 | 29 | import AkkaMetricRegistry._ 30 | 31 | def actorCount(system: String): GaugeWrapper = gauge(ActorCountMetricName, tagSeq(system)) 32 | def deadLetterCount(system: String): Counter = counter(DeadLetterCountMetricName, tagSeq(system)) 33 | def unhandledMessageCount(system: String): Counter = counter(UnhandledMessageCountMetricName, tagSeq(system)) 34 | private def tagSeq(system: String): Iterable[Tag] = Seq(new ImmutableTag(ActorSystem, system)) 35 | } 36 | -------------------------------------------------------------------------------- /src/main/scala/akka/monitor/instrumentation/EnvelopeInstrumentation.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2017,2018 Workday, Inc. 4 | * Copyright © 2013-2017 the kamon project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 7 | * except in compliance with the License. You may obtain a copy of 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 distributed under the 12 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific language governing permissions 14 | * and limitations under the License. 15 | * ========================================================================================= 16 | */ 17 | package akka.monitor.instrumentation 18 | 19 | import org.aspectj.lang.annotation.{ DeclareMixin, Aspect } 20 | 21 | case class EnvelopeContext(nanoTime: Long) 22 | 23 | object EnvelopeContext { 24 | val Empty: EnvelopeContext = EnvelopeContext(0L) 25 | def apply(): EnvelopeContext = EnvelopeContext(System.nanoTime()) 26 | } 27 | 28 | trait InstrumentedEnvelope extends Serializable { 29 | def envelopeContext(): EnvelopeContext 30 | def setEnvelopeContext(envelopeContext: EnvelopeContext): Unit 31 | } 32 | 33 | object InstrumentedEnvelope { 34 | def apply(): InstrumentedEnvelope = new InstrumentedEnvelope { 35 | private var ctx = akka.monitor.instrumentation.EnvelopeContext.Empty 36 | 37 | override def envelopeContext(): EnvelopeContext = ctx 38 | 39 | override def setEnvelopeContext(envelopeContext: akka.monitor.instrumentation.EnvelopeContext): Unit = 40 | ctx = envelopeContext 41 | } 42 | } 43 | 44 | @Aspect 45 | class EnvelopeContextIntoEnvelopeMixin { 46 | 47 | @DeclareMixin("akka.dispatch.Envelope") 48 | def mixinInstrumentationToEnvelope: InstrumentedEnvelope = InstrumentedEnvelope() 49 | } 50 | -------------------------------------------------------------------------------- /src/main/scala/akka/monitor/instrumentation/DeadLettersInstrumentation.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2017,2018 Workday, Inc. 4 | * Copyright © 2013-2017 the kamon project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 7 | * except in compliance with the License. You may obtain a copy of 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 distributed under the 12 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific language governing permissions 14 | * and limitations under the License. 15 | * ========================================================================================= 16 | */ 17 | package akka.monitor.instrumentation 18 | 19 | import akka.actor.{DeadLetter, UnhandledMessage} 20 | import io.kontainers.micrometer.akka.{ActorSystemMetrics, MetricsConfig} 21 | import org.aspectj.lang.annotation.{After, Aspect, Pointcut} 22 | 23 | @Aspect 24 | class DeadLettersInstrumentation { 25 | 26 | @Pointcut("call(void akka.event.EventStream.publish(Object)) && args(event)") 27 | def streamPublish(event: Object): Unit = {} 28 | 29 | @After("streamPublish(event)") 30 | def afterStreamSubchannel(event: Object): Unit = { 31 | trackEvent(event) 32 | } 33 | 34 | private def trackEvent(event: Object): Unit = { 35 | if (MetricsConfig.matchEvents) { 36 | event match { 37 | case dl: DeadLetter => { 38 | val systemName = dl.sender.path.address.system 39 | ActorSystemMetrics.deadLetterCount(systemName).increment() 40 | } 41 | case um: UnhandledMessage => { 42 | val systemName = um.sender.path.address.system 43 | ActorSystemMetrics.unhandledMessageCount(systemName).increment() 44 | } 45 | case _ => 46 | } 47 | } 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /src/main/scala/io/kontainers/micrometer/akka/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2017,2018 Workday, Inc. 4 | * Copyright © 2013-2017 the kamon project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 7 | * except in compliance with the License. You may obtain a copy of 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 distributed under the 12 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific language governing permissions 14 | * and limitations under the License. 15 | * ========================================================================================= 16 | */ 17 | package io.kontainers.micrometer 18 | 19 | import java.util.regex.Pattern 20 | 21 | import scala.annotation.tailrec 22 | 23 | package object akka { 24 | def metricFriendlyActorName(actorPath: String) = { 25 | sanitizeMetricName(trimLeadingSlashes(actorPath).toLowerCase.replace("/", "_")) 26 | } 27 | 28 | private val SANITIZE_PREFIX_PATTERN = Pattern.compile("^[^a-zA-Z_]") 29 | private val SANITIZE_BODY_PATTERN = Pattern.compile("[^a-zA-Z0-9_]") 30 | 31 | // borrowed from io.prometheus.client.Collector 32 | def sanitizeMetricName(metricName: String): String = { 33 | SANITIZE_BODY_PATTERN.matcher(SANITIZE_PREFIX_PATTERN.matcher(metricName).replaceFirst("_")).replaceAll("_") 34 | } 35 | 36 | @tailrec 37 | private def trimLeadingSlashes(s: String): String = { 38 | if (s.startsWith("/")) trimLeadingSlashes(s.substring(1)) else s 39 | } 40 | 41 | type ForkJoinPoolLike = { 42 | def getParallelism: Int 43 | def getPoolSize: Int 44 | def getActiveThreadCount: Int 45 | def getRunningThreadCount: Int 46 | def getQueuedSubmissionCount: Int 47 | def getQueuedTaskCount: Long 48 | def getStealCount: Long 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/test/scala/io/kontainers/micrometer/akka/ActorMetricsTestActor.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2017,2018 Workday, Inc. 4 | * Copyright © 2013-2017 the kamon project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 7 | * except in compliance with the License. You may obtain a copy of 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 distributed under the 12 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific language governing permissions 14 | * and limitations under the License. 15 | * ========================================================================================= 16 | */ 17 | package io.kontainers.micrometer.akka 18 | 19 | import akka.actor._ 20 | import scala.concurrent.duration._ 21 | 22 | class ActorMetricsTestActor extends Actor { 23 | import ActorMetricsTestActor._ 24 | 25 | override def receive = { 26 | case Discard => 27 | case Fail => throw new ArithmeticException("Division by zero.") 28 | case Ping => sender() ! Pong 29 | case TrackTimings(sendTimestamp, sleep) => { 30 | val dequeueTimestamp = System.nanoTime() 31 | sleep.map(s => Thread.sleep(s.toMillis)) 32 | val afterReceiveTimestamp = System.nanoTime() 33 | 34 | sender() ! TrackedTimings(sendTimestamp, dequeueTimestamp, afterReceiveTimestamp) 35 | } 36 | } 37 | } 38 | 39 | object ActorMetricsTestActor { 40 | case object Ping 41 | case object Pong 42 | case object Fail 43 | case object Discard 44 | 45 | case class TrackTimings(sendTimestamp: Long = System.nanoTime(), sleep: Option[Duration] = None) 46 | case class TrackedTimings(sendTimestamp: Long, dequeueTimestamp: Long, afterReceiveTimestamp: Long) { 47 | def approximateTimeInMailbox: Long = dequeueTimestamp - sendTimestamp 48 | def approximateProcessingTime: Long = afterReceiveTimestamp - dequeueTimestamp 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/scala/io/kontainers/micrometer/akka/ActorMetrics.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2017,2018 Workday, Inc. 4 | * Copyright © 2013-2017 the kamon project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 7 | * except in compliance with the License. You may obtain a copy of 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 distributed under the 12 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific language governing permissions 14 | * and limitations under the License. 15 | * ========================================================================================= 16 | */ 17 | package io.kontainers.micrometer.akka 18 | 19 | import scala.collection.concurrent.TrieMap 20 | import scala.util.control.NonFatal 21 | 22 | import org.slf4j.LoggerFactory 23 | 24 | object ActorMetrics { 25 | private val logger = LoggerFactory.getLogger(ActorMetrics.getClass) 26 | private val map = TrieMap[Entity, ActorMetrics]() 27 | def metricsFor(e: Entity): Option[ActorMetrics] = { 28 | try { 29 | Some(map.getOrElseUpdate(e, new ActorMetrics(e))) 30 | } catch { 31 | case NonFatal(t) => { 32 | logger.warn("Issue with getOrElseUpdate (failing over to simple get)", t) 33 | map.get(e) 34 | } 35 | } 36 | } 37 | def hasMetricsFor(e: Entity): Boolean = map.contains(e) 38 | } 39 | 40 | class ActorMetrics(entity: Entity) { 41 | import AkkaMetricRegistry._ 42 | val actorName = metricFriendlyActorName(entity.name) 43 | val mailboxSize = gauge(s"akka_actor_mailbox_size_$actorName", Seq.empty) 44 | val processingTime = timer(s"akka_actor_processing_time_$actorName", Seq.empty) 45 | val timeInMailbox = timer(s"akka_actor_time_in_mailbox_$actorName", Seq.empty) 46 | val messages = counter(s"akka_actor_message_count_$actorName", Seq.empty) 47 | val errors = counter(s"akka_actor_error_count_$actorName", Seq.empty) 48 | } 49 | -------------------------------------------------------------------------------- /src/test/scala/io/kontainers/micrometer/akka/RouterMetricsTestActor.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2017,2018 Workday, Inc. 4 | * Copyright © 2013-2017 the kamon project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 7 | * except in compliance with the License. You may obtain a copy of 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 distributed under the 12 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific language governing permissions 14 | * and limitations under the License. 15 | * ========================================================================================= 16 | */ 17 | package io.kontainers.micrometer.akka 18 | 19 | import scala.concurrent.duration.Duration 20 | 21 | import akka.actor.Actor 22 | 23 | class RouterMetricsTestActor extends Actor { 24 | import RouterMetricsTestActor._ 25 | override def receive = { 26 | case Discard => 27 | case Fail => throw new ArithmeticException("Division by zero.") 28 | case Ping => sender() ! Pong 29 | case RouterTrackTimings(sendTimestamp, sleep) => { 30 | val dequeueTimestamp = System.nanoTime() 31 | sleep.map(s => Thread.sleep(s.toMillis)) 32 | val afterReceiveTimestamp = System.nanoTime() 33 | 34 | sender() ! RouterTrackedTimings(sendTimestamp, dequeueTimestamp, afterReceiveTimestamp) 35 | } 36 | } 37 | } 38 | 39 | object RouterMetricsTestActor { 40 | case object Ping 41 | case object Pong 42 | case object Fail 43 | case object Discard 44 | 45 | case class RouterTrackTimings(sendTimestamp: Long = System.nanoTime(), sleep: Option[Duration] = None) 46 | case class RouterTrackedTimings(sendTimestamp: Long, dequeueTimestamp: Long, afterReceiveTimestamp: Long) { 47 | def approximateTimeInMailbox: Long = dequeueTimestamp - sendTimestamp 48 | def approximateProcessingTime: Long = afterReceiveTimestamp - dequeueTimestamp 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/scala/io/kontainers/micrometer/akka/RouterMetrics.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2017,2018 Workday, Inc. 4 | * Copyright © 2013-2017 the kamon project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 7 | * except in compliance with the License. You may obtain a copy of 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 distributed under the 12 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific language governing permissions 14 | * and limitations under the License. 15 | * ========================================================================================= 16 | */ 17 | package io.kontainers.micrometer.akka 18 | 19 | import scala.collection.concurrent.TrieMap 20 | import scala.util.control.NonFatal 21 | 22 | import org.slf4j.LoggerFactory 23 | 24 | object RouterMetrics { 25 | private val logger = LoggerFactory.getLogger(RouterMetrics.getClass) 26 | private val map = TrieMap[Entity, RouterMetrics]() 27 | def metricsFor(e: Entity): Option[RouterMetrics] = { 28 | try { 29 | Some(map.getOrElseUpdate(e, new RouterMetrics(e))) 30 | } catch { 31 | case NonFatal(t) => { 32 | logger.warn("Issue with getOrElseUpdate (failing over to simple get)", t) 33 | map.get(e) 34 | } 35 | } 36 | } 37 | def hasMetricsFor(e: Entity): Boolean = map.contains(e) 38 | } 39 | 40 | class RouterMetrics(entity: Entity) { 41 | import io.kontainers.micrometer.akka.AkkaMetricRegistry._ 42 | val actorName = metricFriendlyActorName(entity.name) 43 | val routingTime = timer(s"akka_router_routing_time_$actorName", Seq.empty) 44 | val processingTime = timer(s"akka_router_processing_time_$actorName", Seq.empty) 45 | val timeInMailbox = timer(s"akka_router_time_in_mailbox_$actorName", Seq.empty) 46 | val messages = counter(s"akka_router_message_count_$actorName", Seq.empty) 47 | val errors = counter(s"akka_router_error_count_$actorName", Seq.empty) 48 | } 49 | -------------------------------------------------------------------------------- /src/main/scala/io/kontainers/micrometer/akka/ActorGroupMetrics.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2017, 2018 Workday, Inc. 4 | * Copyright © 2013-2017 the kamon project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 7 | * except in compliance with the License. You may obtain a copy of 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 distributed under the 12 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific language governing permissions 14 | * and limitations under the License. 15 | * ========================================================================================= 16 | */ 17 | package io.kontainers.micrometer.akka 18 | 19 | import io.micrometer.core.instrument.{ImmutableTag, Tag} 20 | 21 | object ActorGroupMetrics { 22 | 23 | val GroupName = "groupName" 24 | 25 | private[akka] val MailboxMetricName = "akka_actor_group_mailboxes_size" 26 | private[akka] val ProcessingTimeMetricName = "akka_actor_group_processing_time" 27 | private[akka] val TimeInMailboxMetricName = "akka_actor_group_time_in_mailboxes" 28 | private[akka] val MessageCountMetricName = "akka_actor_group_message_count" 29 | private[akka] val ActorCountMetricName = "akka_actor_group_actor_count" 30 | private[akka] val ErrorCountMetricName = "akka_actor_group_error_count" 31 | 32 | import AkkaMetricRegistry._ 33 | 34 | def mailboxSize(group: String) = gauge(MailboxMetricName, tagSeq(group)) 35 | def processingTime(group: String) = timer(ProcessingTimeMetricName, tagSeq(group)) 36 | def timeInMailbox(group: String) = timer(TimeInMailboxMetricName, tagSeq(group)) 37 | def messages(group: String) = counter(MessageCountMetricName, tagSeq(group)) 38 | def actorCount(group: String) = gauge(ActorCountMetricName, tagSeq(group)) 39 | def errors(group: String) = counter(ErrorCountMetricName, tagSeq(group)) 40 | private def tagSeq(group: String): Iterable[Tag] = Seq(new ImmutableTag(GroupName, group)) 41 | } 42 | -------------------------------------------------------------------------------- /src/test/scala/io/kontainers/micrometer/akka/MetricsConfigSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2017,2018 Workday, Inc. 4 | * Copyright © 2013-2017 the kamon project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 7 | * except in compliance with the License. You may obtain a copy of 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 distributed under the 12 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific language governing permissions 14 | * and limitations under the License. 15 | * ========================================================================================= 16 | */ 17 | package io.kontainers.micrometer.akka 18 | 19 | class MetricsConfigSpec extends BaseSpec { 20 | "MetricsConfig" should { 21 | "contain the expected group names" in { 22 | MetricsConfig.groupNames should contain allOf ("all", "tracked", "empty", "exclusive") 23 | } 24 | "track correct actor groups" in { 25 | MetricsConfig.actorShouldBeTrackedUnderGroups("system1/hello/MyActor1") should contain theSameElementsAs List("all", "exclusive") 26 | MetricsConfig.actorShouldBeTrackedUnderGroups("system1/hello/NotMyActor1") should contain theSameElementsAs List("all") 27 | } 28 | "track correct actors" in { 29 | MetricsConfig.shouldTrack(MetricsConfig.Actor, "system1/user/tracked-actor1") shouldBe true 30 | MetricsConfig.shouldTrack(MetricsConfig.Actor, "system1/user/non-tracked-actor1") shouldBe false 31 | } 32 | "track correct routers" in { 33 | MetricsConfig.shouldTrack(MetricsConfig.Router, "system1/user/tracked-pool-router") shouldBe true 34 | MetricsConfig.shouldTrack(MetricsConfig.Router, "system1/user/non-tracked-pool-router") shouldBe false 35 | } 36 | "track correct dispatchers" in { 37 | MetricsConfig.shouldTrack(MetricsConfig.Dispatcher, "system1/hello/MyDispatcher1") shouldBe true 38 | MetricsConfig.shouldTrack(MetricsConfig.Dispatcher, "system1/hello/explicitly-excluded") shouldBe false 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/aop.xml: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/main/scala/akka/monitor/instrumentation/CellInfo.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2017,2018 Workday, Inc. 4 | * Copyright © 2013-2017 the kamon project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 7 | * except in compliance with the License. You may obtain a copy of 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 distributed under the 12 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific language governing permissions 14 | * and limitations under the License. 15 | * ========================================================================================= 16 | */ 17 | package akka.monitor.instrumentation 18 | 19 | import io.kontainers.micrometer.akka.{Entity, MetricsConfig} 20 | 21 | import akka.actor.{ActorRef, ActorSystem, Cell} 22 | import akka.routing.{NoRouter, RoutedActorRef} 23 | 24 | case class CellInfo(entity: Entity, actorSystemName: String, isRouter: Boolean, isRoutee: Boolean, isTracked: Boolean, 25 | trackingGroups: List[String], actorCellCreation: Boolean) 26 | 27 | object CellInfo { 28 | 29 | def cellName(system: ActorSystem, ref: ActorRef): String = 30 | s"""${system.name}/${ref.path.elements.mkString("/")}""" 31 | 32 | def cellInfoFor(cell: Cell, system: ActorSystem, ref: ActorRef, parent: ActorRef, actorCellCreation: Boolean): CellInfo = { 33 | def hasRouterProps(cell: Cell): Boolean = cell.props.deploy.routerConfig != NoRouter 34 | 35 | val pathString = ref.path.elements.mkString("/") 36 | val isRootSupervisor = pathString.length == 0 || pathString == "user" || pathString == "system" 37 | val isRouter = hasRouterProps(cell) 38 | val isRoutee = parent.isInstanceOf[RoutedActorRef] 39 | 40 | val name = if (isRoutee) cellName(system, parent) else cellName(system, ref) 41 | val category = if (isRouter || isRoutee) MetricsConfig.Router else MetricsConfig.Actor 42 | val entity = Entity(name, category) 43 | val isTracked = !isRootSupervisor && MetricsConfig.shouldTrack(category, name) 44 | val trackingGroups = if(isRoutee && isRootSupervisor) List() else MetricsConfig.actorShouldBeTrackedUnderGroups(name) 45 | 46 | CellInfo(entity, system.name, isRouter, isRoutee, isTracked, trackingGroups, actorCellCreation) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/test/scala/io/kontainers/micrometer/akka/ForkJoinPoolMetricsSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2017,2018 Workday, Inc. 4 | * Copyright © 2013-2017 the kamon project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 7 | * except in compliance with the License. You may obtain a copy of 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 distributed under the 12 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific language governing permissions 14 | * and limitations under the License. 15 | * ========================================================================================= 16 | */ 17 | package io.kontainers.micrometer.akka 18 | 19 | import org.slf4j.LoggerFactory 20 | 21 | class ForkJoinPoolMetricsSpec extends BaseSpec { 22 | 23 | val logger = LoggerFactory.getLogger(classOf[ForkJoinPoolMetricsSpec]) 24 | 25 | override def beforeAll(): Unit = { 26 | super.beforeAll() 27 | AkkaMetricRegistry.clear() 28 | } 29 | 30 | "ForkJoinPoolMetrics" should { 31 | "support java forkjoinpool" in { 32 | val name = "ForkJoinPoolMetricsSpec-java-pool" 33 | val pool = new java.util.concurrent.ForkJoinPool 34 | try { 35 | ForkJoinPoolMetrics.add(name, pool.asInstanceOf[ForkJoinPoolLike]) 36 | DispatcherMetricsSpec.findDispatcherRecorder(name, "ForkJoinPool", false) should not be(empty) 37 | } finally { 38 | pool.shutdownNow() 39 | } 40 | } 41 | 42 | "support scala forkjoinpool" in { 43 | try { 44 | val clazz = Class.forName("scala.concurrent.forkjoin.ForkJoinPool") 45 | val name = "ForkJoinPoolMetricsSpec-scala-pool" 46 | val pool = clazz.newInstance 47 | try { 48 | ForkJoinPoolMetrics.add(name, pool.asInstanceOf[ForkJoinPoolLike]) 49 | DispatcherMetricsSpec.findDispatcherRecorder(name, "ForkJoinPool", false) should not be (empty) 50 | } finally { 51 | val method = clazz.getMethod("shutdownNow") 52 | method.invoke(pool) 53 | } 54 | } catch { 55 | case _: ClassNotFoundException => { 56 | logger.warn("skipping scala forkjoinpool test as class no longer supported") 57 | } 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/test/scala/akka/monitor/instrumentation/EnvelopeSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2017,2018 Workday, Inc. 4 | * Copyright © 2017 the kamon project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 7 | * except in compliance with the License. You may obtain a copy of 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 distributed under the 12 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific language governing permissions 14 | * and limitations under the License. 15 | * ========================================================================================= 16 | */ 17 | 18 | package akka.monitor.instrumentation 19 | 20 | import akka.actor.{Actor, ExtendedActorSystem, Props} 21 | import akka.dispatch.Envelope 22 | import io.kontainers.micrometer.akka.TestKitBaseSpec 23 | 24 | class EnvelopeSpec extends TestKitBaseSpec("envelope-spec") { 25 | 26 | "EnvelopeInstrumentation" should { 27 | "mixin EnvelopeContext" in { 28 | val actorRef = system.actorOf(Props[NoReply]()) 29 | val env = Envelope("msg", actorRef, system).asInstanceOf[Object] 30 | env match { 31 | case e: Envelope with InstrumentedEnvelope => e.setEnvelopeContext(EnvelopeContext()) 32 | case _ => fail("InstrumentedEnvelope is not mixed in") 33 | } 34 | env match { 35 | case s: Serializable => { 36 | import java.io._ 37 | val bos = new ByteArrayOutputStream 38 | val oos = new ObjectOutputStream(bos) 39 | oos.writeObject(env) 40 | oos.close() 41 | akka.serialization.JavaSerializer.currentSystem.withValue(system.asInstanceOf[ExtendedActorSystem]) { 42 | val ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray())) 43 | val obj = ois.readObject() 44 | ois.close() 45 | obj match { 46 | case e: Envelope with InstrumentedEnvelope => e.envelopeContext() should not be null 47 | case _ => fail("InstrumentedEnvelope is not mixed in") 48 | } 49 | } 50 | } 51 | case _ => fail("envelope is not serializable") 52 | } 53 | } 54 | } 55 | } 56 | 57 | class NoReply extends Actor { 58 | override def receive = { 59 | case any => 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/test/scala/io/kontainers/micrometer/akka/impl/RegexPathFilterSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2017,2018 Workday, Inc. 4 | * Copyright © 2013-2017 the kamon project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 7 | * except in compliance with the License. You may obtain a copy of 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 distributed under the 12 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific language governing permissions 14 | * and limitations under the License. 15 | * ========================================================================================= 16 | */ 17 | package io.kontainers.micrometer.akka.impl 18 | 19 | import org.scalatest.matchers.should.Matchers 20 | import org.scalatest.wordspec.AnyWordSpecLike 21 | 22 | class RegexPathFilterSpec extends AnyWordSpecLike with Matchers { 23 | "The RegexPathFilter" should { 24 | 25 | "match a single expression" in { 26 | val filter = new RegexPathFilter("/user/actor") 27 | 28 | filter.accept("/user/actor") shouldBe true 29 | 30 | filter.accept("/user/actor/something") shouldBe false 31 | filter.accept("/user/actor/somethingElse") shouldBe false 32 | } 33 | 34 | "match arbitrary expressions ending with wildcard" in { 35 | val filter = new RegexPathFilter("/user/.*") 36 | 37 | filter.accept("/user/actor") shouldBe true 38 | filter.accept("/user/otherActor") shouldBe true 39 | filter.accept("/user/something/actor") shouldBe true 40 | filter.accept("/user/something/otherActor") shouldBe true 41 | 42 | filter.accept("/otheruser/actor") shouldBe false 43 | filter.accept("/otheruser/otherActor") shouldBe false 44 | filter.accept("/otheruser/something/actor") shouldBe false 45 | filter.accept("/otheruser/something/otherActor") shouldBe false 46 | } 47 | 48 | "match numbers" in { 49 | val filter = new RegexPathFilter("/user/actor-\\d") 50 | 51 | filter.accept("/user/actor-1") shouldBe true 52 | filter.accept("/user/actor-2") shouldBe true 53 | filter.accept("/user/actor-3") shouldBe true 54 | 55 | filter.accept("/user/actor-one") shouldBe false 56 | filter.accept("/user/actor-two") shouldBe false 57 | filter.accept("/user/actor-tree") shouldBe false 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/scala/akka/monitor/instrumentation/RouterMonitor.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2017,2018 Workday, Inc. 4 | * Copyright © 2013-2017 the kamon project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 7 | * except in compliance with the License. You may obtain a copy of 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 distributed under the 12 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific language governing permissions 14 | * and limitations under the License. 15 | * ========================================================================================= 16 | */ 17 | package akka.monitor.instrumentation 18 | 19 | import org.aspectj.lang.ProceedingJoinPoint 20 | 21 | import akka.actor.Cell 22 | import io.kontainers.micrometer.akka.{Entity, RouterMetrics} 23 | 24 | trait RouterMonitor { 25 | def processMessage(pjp: ProceedingJoinPoint): AnyRef 26 | def processFailure(failure: Throwable): Unit 27 | def cleanup(): Unit 28 | 29 | def routeeAdded(): Unit 30 | def routeeRemoved(): Unit 31 | } 32 | 33 | object RouterMonitor { 34 | 35 | def createRouterInstrumentation(cell: Cell): RouterMonitor = { 36 | val cellInfo = CellInfo.cellInfoFor(cell, cell.system, cell.self, cell.parent, false) 37 | if (cellInfo.isTracked) { 38 | RouterMetrics.metricsFor(cellInfo.entity) match { 39 | case Some(rm) => new MetricsOnlyRouterMonitor(cellInfo.entity, rm) 40 | case _ => NoOpRouterMonitor 41 | } 42 | } 43 | else { 44 | NoOpRouterMonitor 45 | } 46 | } 47 | } 48 | 49 | object NoOpRouterMonitor extends RouterMonitor { 50 | def processMessage(pjp: ProceedingJoinPoint): AnyRef = pjp.proceed() 51 | def processFailure(failure: Throwable): Unit = {} 52 | def routeeAdded(): Unit = {} 53 | def routeeRemoved(): Unit = {} 54 | def cleanup(): Unit = {} 55 | } 56 | 57 | class MetricsOnlyRouterMonitor(entity: Entity, routerMetrics: RouterMetrics) extends RouterMonitor { 58 | 59 | def processMessage(pjp: ProceedingJoinPoint): AnyRef = { 60 | val timer = routerMetrics.routingTime.startTimer() 61 | try { 62 | pjp.proceed() 63 | } finally { 64 | timer.close() 65 | } 66 | } 67 | 68 | def processFailure(failure: Throwable): Unit = {} 69 | def routeeAdded(): Unit = {} 70 | def routeeRemoved(): Unit = {} 71 | def cleanup(): Unit = {} 72 | } 73 | -------------------------------------------------------------------------------- /src/main/scala-2/io/kontainers/micrometer/akka/ForkJoinPoolMetrics.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2017,2018 Workday, Inc. 4 | * Copyright © 2013-2017 the kamon project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 7 | * except in compliance with the License. You may obtain a copy of 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 distributed under the 12 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific language governing permissions 14 | * and limitations under the License. 15 | * ========================================================================================= 16 | */ 17 | package io.kontainers.micrometer.akka 18 | 19 | import io.kontainers.micrometer.akka.impl.DoubleFunction 20 | import io.micrometer.core.instrument.{ImmutableTag, Tag} 21 | 22 | import scala.collection.JavaConverters._ 23 | 24 | object ForkJoinPoolMetrics { 25 | val DispatcherName = "dispatcherName" 26 | 27 | def add(dispatcherName: String, fjp: ForkJoinPoolLike): Unit = { 28 | import io.kontainers.micrometer.akka.AkkaMetricRegistry._ 29 | val tags: Iterable[Tag] = Seq(new ImmutableTag(DispatcherName, dispatcherName)) 30 | val jtags = tags.asJava 31 | val parellelismFn = new DoubleFunction[ForkJoinPoolLike](_.getParallelism) 32 | val poolSizeFn = new DoubleFunction[ForkJoinPoolLike](_.getParallelism) 33 | val activeThreadCountFn = new DoubleFunction[ForkJoinPoolLike](_.getActiveThreadCount) 34 | val runningThreadCountFn = new DoubleFunction[ForkJoinPoolLike](_.getRunningThreadCount) 35 | val queuedSubmissionCountFn = new DoubleFunction[ForkJoinPoolLike](_.getQueuedSubmissionCount) 36 | val queuedTaskCountFn = new DoubleFunction[ForkJoinPoolLike](_.getQueuedTaskCount) 37 | val stealCountFn = new DoubleFunction[ForkJoinPoolLike](_.getStealCount) 38 | getRegistry.gauge("akka_dispatcher_forkjoinpool_parellelism", jtags, fjp, parellelismFn) 39 | getRegistry.gauge("akka_dispatcher_forkjoinpool_pool_size", jtags, fjp, poolSizeFn) 40 | getRegistry.gauge("akka_dispatcher_forkjoinpool_active_thread_count", jtags, fjp, activeThreadCountFn) 41 | getRegistry.gauge("akka_dispatcher_forkjoinpool_running_thread_count", jtags, fjp, runningThreadCountFn) 42 | getRegistry.gauge("akka_dispatcher_forkjoinpool_queued_task_count", jtags, fjp, queuedSubmissionCountFn) 43 | getRegistry.gauge("akka_dispatcher_forkjoinpool_queued_submission_count", jtags, fjp, queuedTaskCountFn) 44 | getRegistry.gauge("akka_dispatcher_forkjoinpool_steal_count", jtags, fjp, stealCountFn) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/scala-3/io/kontainers/micrometer/akka/ForkJoinPoolMetrics.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2017,2018 Workday, Inc. 4 | * Copyright © 2013-2017 the kamon project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 7 | * except in compliance with the License. You may obtain a copy of 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 distributed under the 12 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific language governing permissions 14 | * and limitations under the License. 15 | * ========================================================================================= 16 | */ 17 | package io.kontainers.micrometer.akka 18 | 19 | import io.kontainers.micrometer.akka.impl.DoubleFunction 20 | import io.micrometer.core.instrument.{ImmutableTag, Tag} 21 | 22 | import scala.jdk.CollectionConverters.* 23 | 24 | object ForkJoinPoolMetrics { 25 | val DispatcherName = "dispatcherName" 26 | 27 | def add(dispatcherName: String, fjp: ForkJoinPoolLike): Unit = { 28 | import io.kontainers.micrometer.akka.AkkaMetricRegistry._ 29 | import reflect.Selectable.reflectiveSelectable 30 | val tags: Iterable[Tag] = Seq(new ImmutableTag(DispatcherName, dispatcherName)) 31 | val jtags = tags.asJava 32 | val parellelismFn = new DoubleFunction[ForkJoinPoolLike](_.getParallelism) 33 | val poolSizeFn = new DoubleFunction[ForkJoinPoolLike](_.getParallelism) 34 | val activeThreadCountFn = new DoubleFunction[ForkJoinPoolLike](_.getActiveThreadCount) 35 | val runningThreadCountFn = new DoubleFunction[ForkJoinPoolLike](_.getRunningThreadCount) 36 | val queuedSubmissionCountFn = new DoubleFunction[ForkJoinPoolLike](_.getQueuedSubmissionCount) 37 | val queuedTaskCountFn = new DoubleFunction[ForkJoinPoolLike](_.getQueuedTaskCount) 38 | val stealCountFn = new DoubleFunction[ForkJoinPoolLike](_.getStealCount) 39 | getRegistry.gauge("akka_dispatcher_forkjoinpool_parellelism", jtags, fjp, parellelismFn) 40 | getRegistry.gauge("akka_dispatcher_forkjoinpool_pool_size", jtags, fjp, poolSizeFn) 41 | getRegistry.gauge("akka_dispatcher_forkjoinpool_active_thread_count", jtags, fjp, activeThreadCountFn) 42 | getRegistry.gauge("akka_dispatcher_forkjoinpool_running_thread_count", jtags, fjp, runningThreadCountFn) 43 | getRegistry.gauge("akka_dispatcher_forkjoinpool_queued_task_count", jtags, fjp, queuedSubmissionCountFn) 44 | getRegistry.gauge("akka_dispatcher_forkjoinpool_queued_submission_count", jtags, fjp, queuedTaskCountFn) 45 | getRegistry.gauge("akka_dispatcher_forkjoinpool_steal_count", jtags, fjp, stealCountFn) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/scala/io/kontainers/micrometer/akka/ThreadPoolMetrics.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2017, 2018 Workday, Inc. 4 | * Copyright © 2013-2017 the kamon project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 7 | * except in compliance with the License. You may obtain a copy of 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 distributed under the 12 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific language governing permissions 14 | * and limitations under the License. 15 | * ========================================================================================= 16 | */ 17 | package io.kontainers.micrometer.akka 18 | 19 | import java.util.concurrent.ThreadPoolExecutor 20 | 21 | import scala.collection.JavaConverters._ 22 | 23 | import io.kontainers.micrometer.akka.impl.DoubleFunction 24 | import io.micrometer.core.instrument.{ImmutableTag, Tag} 25 | 26 | object ThreadPoolMetrics { 27 | 28 | val DispatcherName = "dispatcherName" 29 | 30 | def add(dispatcherName: String, tpe: ThreadPoolExecutor): Unit = { 31 | import io.kontainers.micrometer.akka.AkkaMetricRegistry._ 32 | val tags: Iterable[Tag] = Seq(new ImmutableTag(DispatcherName, dispatcherName)) 33 | val jtags = tags.asJava 34 | val activeCountFn = new DoubleFunction[ThreadPoolExecutor](_.getActiveCount) 35 | val corePoolSizeFn = new DoubleFunction[ThreadPoolExecutor](_.getCorePoolSize) 36 | val poolSizeFn = new DoubleFunction[ThreadPoolExecutor](_.getPoolSize) 37 | val largestPoolSizeFn = new DoubleFunction[ThreadPoolExecutor](_.getLargestPoolSize) 38 | val maximumPoolSizeFn = new DoubleFunction[ThreadPoolExecutor](_.getMaximumPoolSize) 39 | val completedCountFn = new DoubleFunction[ThreadPoolExecutor](_.getCompletedTaskCount) 40 | val taskCountFn = new DoubleFunction[ThreadPoolExecutor](_.getTaskCount) 41 | getRegistry.gauge("akka_dispatcher_threadpoolexecutor_active_thread_count", jtags, tpe, activeCountFn) 42 | getRegistry.gauge("akka_dispatcher_threadpoolexecutor_core_pool_size", jtags, tpe, corePoolSizeFn) 43 | getRegistry.gauge("akka_dispatcher_threadpoolexecutor_current_pool_size", jtags, tpe, poolSizeFn) 44 | getRegistry.gauge("akka_dispatcher_threadpoolexecutor_largest_pool_size", jtags, tpe, largestPoolSizeFn) 45 | getRegistry.gauge("akka_dispatcher_threadpoolexecutor_max_pool_size", jtags, tpe, maximumPoolSizeFn) 46 | getRegistry.gauge("akka_dispatcher_threadpoolexecutor_completed_task_count", jtags, tpe, completedCountFn) 47 | getRegistry.gauge("akka_dispatcher_threadpoolexecutor_total_task_count", jtags, tpe, taskCountFn) 48 | } 49 | } -------------------------------------------------------------------------------- /src/test/scala/io/kontainers/micrometer/akka/impl/GlobPathFilterSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2017,2018 Workday, Inc. 4 | * Copyright © 2013-2017 the kamon project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 7 | * except in compliance with the License. You may obtain a copy of 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 distributed under the 12 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific language governing permissions 14 | * and limitations under the License. 15 | * ========================================================================================= 16 | */ 17 | package io.kontainers.micrometer.akka.impl 18 | 19 | import org.scalatest.matchers.should.Matchers 20 | import org.scalatest.wordspec.AnyWordSpecLike 21 | 22 | class GlobPathFilterSpec extends AnyWordSpecLike with Matchers { 23 | "The GlobPathFilter" should { 24 | 25 | "match a single expression" in { 26 | val filter = new GlobPathFilter("/user/actor") 27 | 28 | filter.accept("/user/actor") shouldBe true 29 | 30 | filter.accept("/user/actor/something") shouldBe false 31 | filter.accept("/user/actor/somethingElse") shouldBe false 32 | } 33 | 34 | "match all expressions in the same level" in { 35 | val filter = new GlobPathFilter("/user/*") 36 | 37 | filter.accept("/user/actor") shouldBe true 38 | filter.accept("/user/otherActor") shouldBe true 39 | 40 | filter.accept("/user/something/actor") shouldBe false 41 | filter.accept("/user/something/otherActor") shouldBe false 42 | } 43 | 44 | "match all expressions" in { 45 | val filter = new GlobPathFilter("**") 46 | 47 | filter.accept("GET: /ping") shouldBe true 48 | filter.accept("GET: /ping/pong") shouldBe true 49 | } 50 | 51 | "match all expressions and crosses the path boundaries" in { 52 | val filter = new GlobPathFilter("/user/actor-**") 53 | 54 | filter.accept("/user/actor-") shouldBe true 55 | filter.accept("/user/actor-one") shouldBe true 56 | filter.accept("/user/actor-one/other") shouldBe true 57 | 58 | filter.accept("/user/something/actor") shouldBe false 59 | filter.accept("/user/something/otherActor") shouldBe false 60 | } 61 | 62 | "match exactly one character" in { 63 | val filter = new GlobPathFilter("/user/actor-?") 64 | 65 | filter.accept("/user/actor-1") shouldBe true 66 | filter.accept("/user/actor-2") shouldBe true 67 | filter.accept("/user/actor-3") shouldBe true 68 | 69 | filter.accept("/user/actor-one") shouldBe false 70 | filter.accept("/user/actor-two") shouldBe false 71 | filter.accept("/user/actor-tree") shouldBe false 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/test/scala/io/kontainers/micrometer/akka/DispatcherMetricsSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2017,2018 Workday, Inc. 4 | * Copyright © 2013-2017 the kamon project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 7 | * except in compliance with the License. You may obtain a copy of 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 distributed under the 12 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific language governing permissions 14 | * and limitations under the License. 15 | * ========================================================================================= 16 | */ 17 | package io.kontainers.micrometer.akka 18 | 19 | import scala.concurrent.Future 20 | 21 | import akka.actor._ 22 | import akka.dispatch.MessageDispatcher 23 | import akka.testkit.TestProbe 24 | import io.kontainers.micrometer.akka.ForkJoinPoolMetrics.DispatcherName 25 | import io.micrometer.core.instrument.Tag 26 | 27 | object DispatcherMetricsSpec { 28 | val SystemName = "DispatcherMetricsSpec" 29 | 30 | def findDispatcherRecorder(dispatcherName: String, 31 | dispatcherType: String = "ForkJoinPool", 32 | useMicrometerExecutorServiceMetrics: Boolean = MetricsConfig.useMicrometerExecutorServiceMetrics): Map[String, Double] = { 33 | val tags = if (useMicrometerExecutorServiceMetrics) { 34 | Seq(Tag.of("name", dispatcherName), Tag.of("type", dispatcherType)) 35 | } else { 36 | Seq(Tag.of(DispatcherName, dispatcherName)) 37 | } 38 | AkkaMetricRegistry.metricsForTags(tags) 39 | } 40 | } 41 | 42 | class DispatcherMetricsSpec extends TestKitBaseSpec(DispatcherMetricsSpec.SystemName) { 43 | 44 | override def beforeAll(): Unit = { 45 | super.beforeAll() 46 | AkkaMetricRegistry.clear() 47 | } 48 | 49 | "the akka dispatcher metrics" should { 50 | "respect the configured include and exclude filters" in { 51 | forceInit(system.dispatchers.lookup("akka.actor.default-dispatcher")) 52 | val fjpDispatcher = forceInit(system.dispatchers.lookup("tracked-fjp")) 53 | val tpeDispatcher = forceInit(system.dispatchers.lookup("tracked-tpe")) 54 | val excludedDispatcher = forceInit(system.dispatchers.lookup("explicitly-excluded")) 55 | 56 | import DispatcherMetricsSpec.findDispatcherRecorder 57 | findDispatcherRecorder(fjpDispatcher.id) shouldNot be(empty) 58 | findDispatcherRecorder(tpeDispatcher.id, "ThreadPoolExecutor") shouldNot be(empty) 59 | findDispatcherRecorder(excludedDispatcher.id) should be(empty) 60 | } 61 | } 62 | 63 | def forceInit(dispatcher: MessageDispatcher): MessageDispatcher = { 64 | val listener = TestProbe() 65 | Future { 66 | listener.ref ! "init done" 67 | }(dispatcher) 68 | listener.expectMsg("init done") 69 | 70 | dispatcher 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/test/scala/io/kontainers/micrometer/akka/ActorMetricsSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2017,2018 Workday, Inc. 4 | * Copyright © 2013-2017 the kamon project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 7 | * except in compliance with the License. You may obtain a copy of 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 distributed under the 12 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific language governing permissions 14 | * and limitations under the License. 15 | * ========================================================================================= 16 | */ 17 | package io.kontainers.micrometer.akka 18 | 19 | import scala.concurrent.duration.DurationInt 20 | import scala.concurrent.{Await, Future} 21 | 22 | import akka.actor._ 23 | import akka.monitor.instrumentation.CellInfo 24 | import akka.testkit.TestProbe 25 | 26 | class ActorMetricsSpec extends TestKitBaseSpec("ActorMetricsSpec") { 27 | 28 | import ActorMetricsTestActor._ 29 | 30 | "the actor metrics" should { 31 | "respect the configured include and exclude filters" in { 32 | val trackedActor = createTestActor("tracked-actor") 33 | val nonTrackedActor = createTestActor("non-tracked-actor") 34 | val excludedTrackedActor = createTestActor("tracked-explicitly-excluded-actor") 35 | 36 | actorMetricsRecorderOf(trackedActor) should not be empty 37 | actorMetricsRecorderOf(nonTrackedActor) shouldBe empty 38 | actorMetricsRecorderOf(excludedTrackedActor) shouldBe empty 39 | 40 | val metrics = actorMetricsRecorderOf(trackedActor).get 41 | metrics.actorName shouldEqual "actormetricsspec_user_tracked_actor" 42 | metrics.messages.count() shouldEqual 1.0 43 | } 44 | 45 | "handle concurrent metric getOrElseUpdate calls" in { 46 | implicit val ec = system.dispatcher 47 | val e = Entity("fake-actor-name", MetricsConfig.Actor) 48 | val futures = (1 to 100).map{ _ => Future(ActorMetrics.metricsFor(e)) } 49 | val future = Future.sequence(futures) 50 | val metrics = Await.result(future, 10.seconds) 51 | metrics.fold(metrics.head) { (compare, metric) => 52 | metric shouldEqual compare 53 | compare 54 | } 55 | } 56 | } 57 | 58 | def actorMetricsRecorderOf(ref: ActorRef): Option[ActorMetrics] = { 59 | val name = CellInfo.cellName(system, ref) 60 | val entity = Entity(name, MetricsConfig.Actor) 61 | if (ActorMetrics.hasMetricsFor(entity)) { 62 | ActorMetrics.metricsFor(entity) 63 | } else { 64 | None 65 | } 66 | } 67 | 68 | def createTestActor(name: String): ActorRef = { 69 | val actor = system.actorOf(Props[ActorMetricsTestActor](), name) 70 | val initialiseListener = TestProbe() 71 | 72 | // Ensure that the router has been created before returning. 73 | actor.tell(Ping, initialiseListener.ref) 74 | initialiseListener.expectMsg(Pong) 75 | 76 | actor 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/test/scala/io/kontainers/micrometer/akka/ActorSystemMetricsSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2017,2018 Workday, Inc. 4 | * Copyright © 2013-2017 the kamon project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 7 | * except in compliance with the License. You may obtain a copy of 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 distributed under the 12 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific language governing permissions 14 | * and limitations under the License. 15 | * ========================================================================================= 16 | */ 17 | package io.kontainers.micrometer.akka 18 | 19 | import akka.actor.Props 20 | import io.kontainers.micrometer.akka.ActorSystemMetrics._ 21 | import io.micrometer.core.instrument.ImmutableTag 22 | import org.scalatest.BeforeAndAfterEach 23 | import org.scalatest.concurrent.Eventually 24 | 25 | import scala.concurrent.duration.DurationInt 26 | 27 | class ActorSystemMetricsSpec extends TestKitBaseSpec("ActorSystemMetricsSpec") with BeforeAndAfterEach with Eventually { 28 | 29 | "the actor system metrics" should { 30 | "count actors" in { 31 | val originalMetrics = findSystemMetricsRecorder(system.name) 32 | val originalCount = originalMetrics.getOrElse(ActorCountMetricName, 0.0) 33 | val trackedActor = system.actorOf(Props[ActorMetricsTestActor]()) 34 | eventually(timeout(5.seconds)) { 35 | val map = findSystemMetricsRecorder(system.name) 36 | map should not be empty 37 | map.getOrElse(ActorCountMetricName, -1.0) shouldEqual (originalCount + 1.0) 38 | } 39 | system.stop(trackedActor) 40 | eventually(timeout(5.seconds)) { 41 | val metrics = findSystemMetricsRecorder(system.name) 42 | metrics.getOrElse(ActorCountMetricName, -1.0) shouldEqual originalCount 43 | } 44 | } 45 | "count unhandled messages" in { 46 | val count = findSystemMetricsRecorder(system.name).getOrElse(UnhandledMessageCountMetricName, 0.0) 47 | val trackedActor = system.actorOf(Props[ActorMetricsTestActor]()) 48 | trackedActor ! "unhandled" 49 | eventually(timeout(5.seconds)) { 50 | findSystemMetricsRecorder(system.name).getOrElse(UnhandledMessageCountMetricName, -1.0) shouldEqual (count + 1.0) 51 | } 52 | } 53 | "count dead letters" in { 54 | val count = findSystemMetricsRecorder(system.name).getOrElse(DeadLetterCountMetricName, 0.0) 55 | val trackedActor = system.actorOf(Props[ActorMetricsTestActor]()) 56 | system.stop(trackedActor) 57 | eventually(timeout(5.seconds)) { 58 | trackedActor ! "dead" 59 | findSystemMetricsRecorder(system.name).getOrElse(DeadLetterCountMetricName, -1.0) shouldBe > (count) 60 | } 61 | } 62 | } 63 | 64 | def findSystemMetricsRecorder(name: String): Map[String, Double] = { 65 | AkkaMetricRegistry.metricsForTags(Seq(new ImmutableTag(ActorSystemMetrics.ActorSystem, name))) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/test/scala/io/kontainers/micrometer/akka/RouterMetricsSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2017,2018 Workday, Inc. 4 | * Copyright © 2013-2017 the kamon project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 7 | * except in compliance with the License. You may obtain a copy of 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 distributed under the 12 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific language governing permissions 14 | * and limitations under the License. 15 | * ========================================================================================= 16 | */ 17 | package io.kontainers.micrometer.akka 18 | 19 | import scala.concurrent.{Await, Future} 20 | import scala.concurrent.duration.DurationInt 21 | 22 | import akka.actor._ 23 | import akka.monitor.instrumentation.CellInfo 24 | import akka.routing._ 25 | import akka.testkit.TestProbe 26 | 27 | class RouterMetricsSpec extends TestKitBaseSpec("RouterMetricsSpec") { 28 | 29 | import RouterMetricsTestActor._ 30 | 31 | "the router metrics" should { 32 | "respect the configured include and exclude filters" in { 33 | val trackedRouter = createTestPoolRouter("tracked-pool-router") 34 | val nonTrackedRouter = createTestPoolRouter("non-tracked-pool-router") 35 | val excludedTrackedRouter = createTestPoolRouter("tracked-explicitly-excluded-pool-router") 36 | 37 | routerMetricsRecorderOf(trackedRouter) should not be empty 38 | routerMetricsRecorderOf(nonTrackedRouter) shouldBe empty 39 | routerMetricsRecorderOf(excludedTrackedRouter) shouldBe empty 40 | 41 | val metrics = routerMetricsRecorderOf(trackedRouter).get 42 | metrics.actorName shouldEqual "routermetricsspec_user_tracked_pool_router" 43 | metrics.messages.count() shouldEqual 1.0 44 | } 45 | 46 | "handle concurrent metric getOrElseUpdate calls" in { 47 | implicit val ec = system.dispatcher 48 | val e = Entity("fake-actor-name", MetricsConfig.Actor) 49 | val futures = (1 to 100).map{ _ => Future(ActorMetrics.metricsFor(e)) } 50 | val future = Future.sequence(futures) 51 | val metrics = Await.result(future, 10.seconds) 52 | metrics.fold(metrics.head) { (compare, metric) => 53 | metric shouldEqual compare 54 | compare 55 | } 56 | } 57 | } 58 | 59 | def routerMetricsRecorderOf(ref: ActorRef): Option[RouterMetrics] = { 60 | val name = CellInfo.cellName(system, ref) 61 | val entity = Entity(name, MetricsConfig.Router) 62 | if (RouterMetrics.hasMetricsFor(entity)) { 63 | RouterMetrics.metricsFor(entity) 64 | } else { 65 | None 66 | } 67 | } 68 | 69 | def createTestPoolRouter(routerName: String): ActorRef = { 70 | val router = system.actorOf(RoundRobinPool(5).props(Props[RouterMetricsTestActor]()), routerName) 71 | val initialiseListener = TestProbe() 72 | 73 | // Ensure that the router has been created before returning. 74 | router.tell(Ping, initialiseListener.ref) 75 | initialiseListener.expectMsg(Pong) 76 | 77 | router 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/scala/akka/monitor/instrumentation/RouterInstrumentation.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2017,2018 Workday, Inc. 4 | * Copyright © 2013-2017 the kamon project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 7 | * except in compliance with the License. You may obtain a copy of 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 distributed under the 12 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific language governing permissions 14 | * and limitations under the License. 15 | * ========================================================================================= 16 | */ 17 | package akka.monitor.instrumentation 18 | 19 | import org.aspectj.lang.ProceedingJoinPoint 20 | import org.aspectj.lang.annotation.{ After, Around, Aspect, DeclareMixin, Pointcut } 21 | 22 | import akka.actor.{ ActorRef, ActorSystem, Cell, Props } 23 | import akka.dispatch.{ Envelope, MessageDispatcher } 24 | import akka.routing.RoutedActorCell 25 | 26 | @Aspect 27 | class RoutedActorCellInstrumentation { 28 | 29 | def routerInstrumentation(cell: Cell): RouterMonitor = 30 | cell.asInstanceOf[RouterInstrumentationAware].routerInstrumentation 31 | 32 | @Pointcut("execution(akka.routing.RoutedActorCell.new(..)) && this(cell) && args(system, ref, props, dispatcher, routeeProps, supervisor)") 33 | def routedActorCellCreation(cell: RoutedActorCell, system: ActorSystem, ref: ActorRef, props: Props, dispatcher: MessageDispatcher, routeeProps: Props, supervisor: ActorRef): Unit = {} 34 | 35 | @After("routedActorCellCreation(cell, system, ref, props, dispatcher, routeeProps, supervisor)") 36 | def afterRoutedActorCellCreation(cell: RoutedActorCell, system: ActorSystem, ref: ActorRef, props: Props, dispatcher: MessageDispatcher, routeeProps: Props, supervisor: ActorRef): Unit = { 37 | cell.asInstanceOf[RouterInstrumentationAware].setRouterInstrumentation( 38 | RouterMonitor.createRouterInstrumentation(cell)) 39 | } 40 | 41 | @Pointcut("execution(* akka.routing.RoutedActorCell.sendMessage(*)) && this(cell) && args(envelope)") 42 | def sendMessageInRouterActorCell(cell: RoutedActorCell, envelope: Envelope) = {} 43 | 44 | @Around("sendMessageInRouterActorCell(cell, envelope)") 45 | def aroundSendMessageInRouterActorCell(pjp: ProceedingJoinPoint, cell: RoutedActorCell, envelope: Envelope): Any = { 46 | routerInstrumentation(cell).processMessage(pjp) 47 | } 48 | } 49 | 50 | trait RouterInstrumentationAware { 51 | def routerInstrumentation: RouterMonitor 52 | def setRouterInstrumentation(ai: RouterMonitor): Unit 53 | } 54 | 55 | object RouterInstrumentationAware { 56 | def apply(): RouterInstrumentationAware = new RouterInstrumentationAware { 57 | private var _ri: RouterMonitor = _ 58 | 59 | override def setRouterInstrumentation(ai: RouterMonitor): Unit = _ri = ai 60 | override def routerInstrumentation: RouterMonitor = _ri 61 | } 62 | } 63 | 64 | @Aspect 65 | class MetricsIntoRouterCellsMixin { 66 | 67 | @DeclareMixin("akka.routing.RoutedActorCell") 68 | def mixinActorCellMetricsToRoutedActorCell: RouterInstrumentationAware = RouterInstrumentationAware() 69 | 70 | } 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/kontainers/micrometer-akka.svg?branch=master)](https://travis-ci.org/kontainers/micrometer-akka) 2 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.kontainers/micrometer-akka_2.13/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.kontainers/micrometer-akka_2.13) 3 | [![codecov.io](https://codecov.io/gh/kontainers/micrometer-akka/coverage.svg?branch=master)](https://codecov.io/gh/kontainers/micrometer-akka/branch/master) 4 | 5 | # micrometer-akka 6 | 7 | This project is a fork of [Kamon-Akka](http://kamon.io/documentation/kamon-akka/0.6.6/overview/). The Kamon team have done a great job and if you are just experimenting with metrics collection, then their tools and documentation are a great starting point. 8 | This fork produces metrics in [Micrometer](http://micrometer.io/) format. 9 | See also [Prometheus-Akka](https://github.com/Workday/prometheus-akka). 10 | 11 | Differences from Kamon-Akka: 12 | - we do not support Kamon TraceContexts, as we currently have no use case for them 13 | - we support Scala 2.11, Scala 2.12 and Scala 2.13 14 | - we only build with Akka 2.5 but we test the build with Akka 2.6 too 15 | - akka 2.4 is supported prior to v0.12.0 16 | - records time in seconds as opposed to nanoseconds (the data is still a double) 17 | 18 | ```sbt 19 | "io.kontainers" %% "micrometer-akka" % "0.12.3" 20 | ``` 21 | 22 | There is a sample project at https://github.com/pjfanning/micrometer-akka-sample 23 | 24 | [Release Notes](https://github.com/kontainers/micrometer-akka/releases) 25 | 26 | ## Usage 27 | 28 | To enable monitoring, include the appropriate jar as a dependency and include the following Java runtime flag in your Java startup command (aspectjweaver is a transitive dependency of micrometer-akka): 29 | 30 | -javaagent:/path/to/aspectjweaver-1.9.7.jar 31 | 32 | You will also need to set up the Micrometer Meter Registry. 33 | 34 | io.kontainers.micrometer.akka.AkkaMetricRegistry#setRegistry ([example](https://github.com/pjfanning/micrometer-akka-sample/blob/master/src/main/scala/com/example/akka/Main.scala)) 35 | 36 | ## Configuration 37 | 38 | The metrics are configured using [application.conf](https://github.com/typesafehub/config) files. There is a default [reference.conf](https://github.com/kontainers/micrometer-akka/blob/master/src/main/resources/reference.conf) that enables only some metrics. 39 | 40 | ### Metrics 41 | 42 | #### Dispatcher 43 | 44 | - differs a little between ForkJoin dispatchers and ThreadPool dispatchers 45 | - ForkJoin: parallelism, activeThreadCount, runningThreadCount, queuedSubmissionCount, queuedTaskCountGauge stealCount 46 | - ThreadPool: activeThreadCount, corePoolSize, currentPoolSize, largestPoolSize, maxPoolSize, completedTaskCount, totalTaskCount 47 | 48 | #### Actor System 49 | 50 | - Actor Count 51 | - Unhandled Message Count 52 | - Dead Letter Count 53 | 54 | #### Actor 55 | 56 | - One metric per actor instance 57 | - mailboxSize (current size), processingTime, timeInMailbox, message count, error count 58 | 59 | #### Actor Router 60 | 61 | - One metric per router instance, summed across all routee actors 62 | - routingTime, timeInMailbox, message count, error count 63 | 64 | #### Actor Group 65 | 66 | - Each actor group has its own include/exclude rules and you can define many groups with individual actors being allowed to be included in many groups - the metrics are summed across all actors in the group 67 | - actorCount (current active actors), mailboxSize (current size), processingTime, timeInMailbox, message count, error count 68 | -------------------------------------------------------------------------------- /src/main/scala/io/kontainers/micrometer/akka/impl/EntityFilter.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2017,2018 Workday, Inc. 4 | * Copyright © 2013-2017 the kamon project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 7 | * except in compliance with the License. You may obtain a copy of 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 distributed under the 12 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific language governing permissions 14 | * and limitations under the License. 15 | * ========================================================================================= 16 | */ 17 | package io.kontainers.micrometer.akka.impl 18 | 19 | import java.util.regex.Pattern 20 | 21 | private[akka] case class EntityFilter(includes: List[PathFilter], excludes: List[PathFilter]) { 22 | def accept(name: String): Boolean = 23 | includes.exists(_.accept(name)) && !excludes.exists(_.accept(name)) 24 | } 25 | 26 | private[akka] trait PathFilter { 27 | def accept(path: String): Boolean 28 | } 29 | 30 | private[akka] case class RegexPathFilter(path: String) extends PathFilter { 31 | private val pathRegex = path.r 32 | override def accept(path: String): Boolean = { 33 | path match { 34 | case pathRegex(_*) => true 35 | case _ => false 36 | } 37 | } 38 | } 39 | 40 | /** 41 | * Default implementation of PathFilter. Uses glob based includes and excludes to determine whether to export. 42 | * 43 | * Get a regular expression pattern which accept any path names which match the given glob. The glob patterns 44 | * function similarly to ant file patterns. 45 | * 46 | * See also: http://ant.apache.org/manual/dirtasks.html#patterns 47 | * 48 | * @author John E. Bailey 49 | * @author David M. Lloyd 50 | */ 51 | private[akka] case class GlobPathFilter(glob: String) extends PathFilter { 52 | private val GLOB_PATTERN = Pattern.compile("(\\*\\*?)|(\\?)|(\\\\.)|(/+)|([^*?]+)") 53 | private val pattern = getGlobPattern(glob) 54 | 55 | def accept(path: String): Boolean = pattern.matcher(path).matches 56 | 57 | private def getGlobPattern(glob: String) = { 58 | val patternBuilder = new StringBuilder 59 | val m = GLOB_PATTERN.matcher(glob) 60 | var lastWasSlash = false 61 | while (m.find) { 62 | lastWasSlash = false 63 | val grp1 = m.group(1) 64 | if (grp1 != null) { 65 | // match a * or ** 66 | if (grp1.length == 2) { 67 | // it's a *workers are able to process multiple metrics* 68 | patternBuilder.append(".*") 69 | } 70 | else { // it's a * 71 | patternBuilder.append("[^/]*") 72 | } 73 | } 74 | else if (m.group(2) != null) { 75 | // match a '?' glob pattern; any non-slash character 76 | patternBuilder.append("[^/]") 77 | } 78 | else if (m.group(3) != null) { 79 | // backslash-escaped value 80 | patternBuilder.append(Pattern.quote(m.group.substring(1))) 81 | } 82 | else if (m.group(4) != null) { 83 | // match any number of / chars 84 | patternBuilder.append("/+") 85 | lastWasSlash = true 86 | } 87 | else { 88 | // some other string 89 | patternBuilder.append(Pattern.quote(m.group)) 90 | } 91 | } 92 | if (lastWasSlash) { 93 | // ends in /, append ** 94 | patternBuilder.append(".*") 95 | } 96 | Pattern.compile(patternBuilder.toString) 97 | } 98 | } -------------------------------------------------------------------------------- /src/main/scala/io/kontainers/micrometer/akka/MetricsConfig.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2017,2018 Workday, Inc. 4 | * Copyright © 2013-2017 the kamon project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 7 | * except in compliance with the License. You may obtain a copy of 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 distributed under the 12 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific language governing permissions 14 | * and limitations under the License. 15 | * ========================================================================================= 16 | */ 17 | package io.kontainers.micrometer.akka 18 | 19 | import scala.collection.JavaConverters._ 20 | import scala.language.postfixOps 21 | 22 | import com.typesafe.config.{Config, ConfigFactory} 23 | import io.kontainers.micrometer.akka.impl.{EntityFilter, GlobPathFilter, RegexPathFilter} 24 | 25 | object MetricsConfig { 26 | private val BaseConfig = "micrometer.akka" 27 | val Dispatcher = "akka-dispatcher" 28 | val Router = "akka-router" 29 | val Actor = "akka-actor" 30 | val ActorGroups = "akka-actor-groups" 31 | 32 | private val defaultConfig = ConfigFactory.defaultApplication().withFallback(ConfigFactory.defaultReferenceUnresolved()) 33 | private val metricFiltersConfig = defaultConfig.getConfig(s"$BaseConfig.metric.filters") 34 | 35 | lazy val matchEvents: Boolean = defaultConfig.getBoolean(s"$BaseConfig.match.events") 36 | lazy val histogramBucketsEnabled: Boolean = defaultConfig.getBoolean(s"$BaseConfig.histogram.buckets.enabled") 37 | lazy val useMicrometerExecutorServiceMetrics: Boolean = { 38 | defaultConfig.getString(s"$BaseConfig.executor-service.style") == "core" 39 | } 40 | 41 | implicit class Syntax(val config: Config) extends AnyVal { 42 | def firstLevelKeys: Set[String] = { 43 | config.entrySet().asScala.map { 44 | case entry => entry.getKey.takeWhile(_ != '.') 45 | } toSet 46 | } 47 | } 48 | 49 | private val filters = createFilters(metricFiltersConfig, metricFiltersConfig.firstLevelKeys.filterNot(_ == ActorGroups)) 50 | private val groupFilters = { 51 | if(metricFiltersConfig.hasPath(ActorGroups)) { 52 | val cfg = metricFiltersConfig.getConfig(ActorGroups) 53 | createFilters(cfg, cfg.firstLevelKeys) 54 | } else { 55 | Map.empty 56 | } 57 | } 58 | 59 | private def createFilters(cfg: Config, categories: Set[String]): Map[String, EntityFilter] = { 60 | import scala.collection.JavaConverters._ 61 | categories map { category => 62 | val asRegex = if (cfg.hasPath(s"$category.asRegex")) cfg.getBoolean(s"$category.asRegex") else false 63 | val includes = cfg.getStringList(s"$category.includes").asScala.map(inc => 64 | if (asRegex) RegexPathFilter(inc) else new GlobPathFilter(inc)).toList 65 | val excludes = cfg.getStringList(s"$category.excludes").asScala.map(exc => 66 | if (asRegex) RegexPathFilter(exc) else new GlobPathFilter(exc)).toList 67 | 68 | (category, EntityFilter(includes, excludes)) 69 | } toMap 70 | } 71 | 72 | def shouldTrack(category: String, entityName: String): Boolean = { 73 | filters.get(category) match { 74 | case Some(filter) => filter.accept(entityName) 75 | case None => false 76 | } 77 | } 78 | 79 | def actorShouldBeTrackedUnderGroups(entityName: String): List[String] = { 80 | val iterable = for((groupName, filter) <- groupFilters if filter.accept(entityName)) yield groupName 81 | iterable.toList 82 | } 83 | 84 | def groupNames: Set[String] = groupFilters.keys.toSet 85 | } 86 | -------------------------------------------------------------------------------- /src/main/scala/io/kontainers/micrometer/akka/AkkaMetricRegistry.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2017,2018 Workday, Inc. 4 | * Copyright © 2013-2017 the kamon project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 7 | * except in compliance with the License. You may obtain a copy of 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 distributed under the 12 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific language governing permissions 14 | * and limitations under the License. 15 | * ========================================================================================= 16 | */ 17 | package io.kontainers.micrometer.akka 18 | 19 | import scala.collection.JavaConverters._ 20 | import scala.collection.concurrent.TrieMap 21 | 22 | import io.micrometer.core.instrument._ 23 | import io.micrometer.core.instrument.simple.SimpleMeterRegistry 24 | 25 | object AkkaMetricRegistry { 26 | private var simpleRegistry = new SimpleMeterRegistry 27 | private var registry: Option[MeterRegistry] = None 28 | private case class MeterKey(name: String, tags: Iterable[Tag]) 29 | private val counterRegistryMap = TrieMap[MeterRegistry, TrieMap[MeterKey, Counter]]() 30 | private val gaugeRegistryMap = TrieMap[MeterRegistry, TrieMap[MeterKey, GaugeWrapper]]() 31 | private val timerRegistryMap = TrieMap[MeterRegistry, TrieMap[MeterKey, Timer]]() 32 | 33 | def getRegistry: MeterRegistry = registry.getOrElse(simpleRegistry) 34 | 35 | def setRegistry(registry: MeterRegistry): Unit = { 36 | this.registry = Option(registry) 37 | } 38 | 39 | def counter(name: String, tags: Iterable[Tag]): Counter = { 40 | def javaTags = tags.asJava 41 | counterMap.getOrElseUpdate(MeterKey(name, tags), getRegistry.counter(name, javaTags)) 42 | } 43 | 44 | def gauge(name: String, tags: Iterable[Tag]): GaugeWrapper = { 45 | gaugeMap.getOrElseUpdate(MeterKey(name, tags), GaugeWrapper(getRegistry, name, tags)) 46 | } 47 | 48 | def timer(name: String, tags: Iterable[Tag]): TimerWrapper = { 49 | def createTimer = { 50 | val builder = Timer.builder(name).tags(tags.asJava) 51 | if (MetricsConfig.histogramBucketsEnabled) { 52 | builder.publishPercentileHistogram() 53 | } 54 | builder.register(getRegistry) 55 | } 56 | TimerWrapper(timerMap.getOrElseUpdate(MeterKey(name, tags), createTimer)) 57 | } 58 | 59 | private[akka] def clear(): Unit = { 60 | timerRegistryMap.clear() 61 | gaugeRegistryMap.clear() 62 | counterRegistryMap.clear() 63 | simpleRegistry.close() 64 | simpleRegistry = new SimpleMeterRegistry() 65 | } 66 | 67 | private[akka] def metricsForTags(tags: Seq[Tag]): Map[String, Double] = { 68 | val tagSet = tags.toSet 69 | val filtered: Iterable[(String, Double)] = getRegistry.getMeters.asScala.flatMap { meter => 70 | val id = meter.getId 71 | if (id.getTags.asScala.toSet == tagSet) { 72 | meter.measure().asScala.headOption.map { measure => 73 | (id.getName, measure.getValue) 74 | } 75 | } else { 76 | None 77 | } 78 | } 79 | filtered.groupBy(_._1).map { case (key, list) => 80 | (key, list.map(_._2).sum) 81 | } 82 | } 83 | 84 | private def counterMap: TrieMap[MeterKey, Counter] = { 85 | counterRegistryMap.getOrElseUpdate(getRegistry, { TrieMap[MeterKey, Counter]() }) 86 | } 87 | 88 | private def gaugeMap: TrieMap[MeterKey, GaugeWrapper] = { 89 | gaugeRegistryMap.getOrElseUpdate(getRegistry, { TrieMap[MeterKey, GaugeWrapper]() }) 90 | } 91 | 92 | private def timerMap: TrieMap[MeterKey, Timer] = { 93 | timerRegistryMap.getOrElseUpdate(getRegistry, { TrieMap[MeterKey, Timer]() }) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/test/scala/io/kontainers/micrometer/akka/ActorGroupMetricsSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2017,2018 Workday, Inc. 4 | * Copyright © 2013-2017 the kamon project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 7 | * except in compliance with the License. You may obtain a copy of 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 distributed under the 12 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific language governing permissions 14 | * and limitations under the License. 15 | * ========================================================================================= 16 | */ 17 | package io.kontainers.micrometer.akka 18 | 19 | import akka.actor._ 20 | import akka.routing.RoundRobinPool 21 | import akka.testkit.TestProbe 22 | import io.micrometer.core.instrument.ImmutableTag 23 | import org.scalatest.BeforeAndAfterEach 24 | import org.scalatest.concurrent.Eventually 25 | 26 | import scala.concurrent.duration.DurationInt 27 | 28 | class ActorGroupMetricsSpec extends TestKitBaseSpec("ActorGroupMetricsSpec") with BeforeAndAfterEach with Eventually { 29 | 30 | import ActorGroupMetrics._ 31 | 32 | override def beforeEach(): Unit = { 33 | super.beforeEach() 34 | clearGroupMetrics 35 | } 36 | 37 | "the actor group metrics" should { 38 | "respect the configured include and exclude filters" in { 39 | val trackedActor = createTestActor("tracked-actor") 40 | val nonTrackedActor = createTestActor("non-tracked-actor") 41 | val excludedTrackedActor = createTestActor("tracked-explicitly-excluded-actor") 42 | 43 | findGroupRecorder("tracked") should not be empty 44 | findGroupRecorder("exclusive") shouldBe empty 45 | val map = findGroupRecorder("tracked") 46 | map.getOrElse(ActorCountMetricName, -1.0) shouldEqual 1.0 47 | map.getOrElse(MessageCountMetricName, -1.0) shouldEqual 1.0 48 | map.getOrElse(MailboxMetricName, -1.0) shouldEqual 0.0 49 | 50 | system.stop(trackedActor) 51 | eventually(timeout(5.seconds)) { 52 | val metrics = findGroupRecorder("tracked") 53 | metrics.getOrElse(ActorCountMetricName, -1.0) shouldEqual 0.0 54 | metrics.getOrElse(ProcessingTimeMetricName, -1.0) should (be >= 0.0) 55 | metrics.getOrElse(ProcessingTimeMetricName, -1.0) should (be <= 1.0) 56 | metrics.getOrElse(TimeInMailboxMetricName, -1.0) should (be >= 0.0) 57 | metrics.getOrElse(TimeInMailboxMetricName, -1.0) should (be <= 1.0) 58 | } 59 | 60 | val trackedActor2 = createTestActor("tracked-actor2") 61 | val trackedActor3 = createTestActor("tracked-actor3") 62 | 63 | val map2 = findGroupRecorder("tracked") 64 | map2.getOrElse(ActorCountMetricName, -1.0) shouldEqual 2.0 65 | map2.getOrElse(MessageCountMetricName, -1.0) shouldBe >=(3.0) 66 | } 67 | 68 | "respect the configured include and exclude filters for routee actors" in { 69 | val trackedRouter = createTestPoolRouter("tracked-router") 70 | val nonTrackedRouter = createTestPoolRouter("non-tracked-router") 71 | val excludedTrackedRouter = createTestPoolRouter("tracked-explicitly-excluded-router") 72 | 73 | findGroupRecorder("tracked") should not be empty 74 | findGroupRecorder("exclusive") shouldBe empty 75 | val map = findGroupRecorder("tracked") 76 | map.getOrElse(ActorCountMetricName, -1.0) shouldEqual 5.0 77 | map.getOrElse(MessageCountMetricName, -1.0) shouldEqual 1.0 78 | //map.getOrElse(MailboxMetricName, -1.0) shouldEqual 0.0 79 | 80 | system.stop(trackedRouter) 81 | eventually(timeout(5.seconds)) { 82 | findGroupRecorder("tracked").getOrElse(ActorCountMetricName, -1.0) shouldEqual 0.0 83 | } 84 | 85 | val trackedRouter2 = createTestPoolRouter("tracked-router2") 86 | val trackedRouter3 = createTestPoolRouter("tracked-router3") 87 | 88 | val map2 = findGroupRecorder("tracked") 89 | map2.getOrElse(ActorCountMetricName, -1.0) shouldEqual 10.0 90 | map2.getOrElse(MessageCountMetricName, -1.0) shouldEqual 3.0 91 | } 92 | } 93 | 94 | def findGroupRecorder(groupName: String): Map[String, Double] = { 95 | AkkaMetricRegistry.metricsForTags(Seq(new ImmutableTag(ActorGroupMetrics.GroupName, groupName))) 96 | } 97 | 98 | def clearGroupMetrics: Unit = { 99 | AkkaMetricRegistry.clear() 100 | } 101 | 102 | def createTestActor(name: String): ActorRef = { 103 | val actor = system.actorOf(Props[ActorMetricsTestActor](), name) 104 | val initialiseListener = TestProbe() 105 | 106 | // Ensure that the router has been created before returning. 107 | import ActorMetricsTestActor._ 108 | actor.tell(Ping, initialiseListener.ref) 109 | initialiseListener.expectMsg(Pong) 110 | 111 | actor 112 | } 113 | 114 | def createTestPoolRouter(routerName: String): ActorRef = { 115 | val router = system.actorOf(RoundRobinPool(5).props(Props[RouterMetricsTestActor]()), routerName) 116 | val initialiseListener = TestProbe() 117 | 118 | // Ensure that the router has been created before returning. 119 | import RouterMetricsTestActor._ 120 | router.tell(Ping, initialiseListener.ref) 121 | initialiseListener.expectMsg(Pong) 122 | 123 | router 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /src/main/scala/akka/monitor/instrumentation/ActorCellInstrumentation.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2017,2018 Workday, Inc. 4 | * Copyright © 2013-2017 the kamon project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 7 | * except in compliance with the License. You may obtain a copy of 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 distributed under the 12 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific language governing permissions 14 | * and limitations under the License. 15 | * ========================================================================================= 16 | */ 17 | package akka.monitor.instrumentation 18 | 19 | import akka.actor.{ActorCell, ActorRef, ActorSystem, Cell, InternalActorRef, UnstartedCell} 20 | import akka.dispatch.Envelope 21 | import akka.dispatch.sysmsg.SystemMessage 22 | import akka.routing.RoutedActorCell 23 | import org.aspectj.lang.ProceedingJoinPoint 24 | import org.aspectj.lang.annotation._ 25 | 26 | import java.util.concurrent.locks.ReentrantLock 27 | import scala.collection.immutable 28 | 29 | @Aspect 30 | class ActorCellInstrumentation { 31 | 32 | private def actorInstrumentation(cell: Cell): ActorMonitor = 33 | cell.asInstanceOf[ActorInstrumentationAware].actorInstrumentation 34 | 35 | @Pointcut("execution(akka.actor.ActorCell.new(..)) && this(cell) && args(system, ref, *, *, parent)") 36 | def actorCellCreation(cell: Cell, system: ActorSystem, ref: ActorRef, parent: InternalActorRef): Unit = {} 37 | 38 | @Pointcut("execution(akka.actor.UnstartedCell.new(..)) && this(cell) && args(system, ref, *, parent)") 39 | def repointableActorRefCreation(cell: Cell, system: ActorSystem, ref: ActorRef, parent: InternalActorRef): Unit = {} 40 | 41 | @After("actorCellCreation(cell, system, ref, parent)") 42 | def afterCreation(cell: Cell, system: ActorSystem, ref: ActorRef, parent: ActorRef): Unit = { 43 | cell.asInstanceOf[ActorInstrumentationAware].setActorInstrumentation( 44 | ActorMonitor.createActorMonitor(cell, system, ref, parent, true)) 45 | } 46 | 47 | @After("repointableActorRefCreation(cell, system, ref, parent)") 48 | def afterRepointableActorRefCreation(cell: Cell, system: ActorSystem, ref: ActorRef, parent: ActorRef): Unit = { 49 | cell.asInstanceOf[ActorInstrumentationAware].setActorInstrumentation( 50 | ActorMonitor.createActorMonitor(cell, system, ref, parent, false)) 51 | } 52 | 53 | @Pointcut("execution(* akka.actor.ActorCell.invoke(*)) && this(cell) && args(envelope)") 54 | def invokingActorBehaviourAtActorCell(cell: ActorCell, envelope: Envelope) = {} 55 | 56 | @Around("invokingActorBehaviourAtActorCell(cell, envelope)") 57 | def aroundBehaviourInvoke(pjp: ProceedingJoinPoint, cell: ActorCell, envelope: Envelope): Any = { 58 | actorInstrumentation(cell).processMessage(pjp, envelope.asInstanceOf[InstrumentedEnvelope].envelopeContext()) 59 | } 60 | 61 | @Pointcut("execution(* akka.dispatch.MessageDispatcher.dispatch(..)) && args(receiver, invocation)") 62 | def sendMessageInActorCell(receiver: ActorCell, invocation: Envelope): Unit = {} 63 | 64 | @Pointcut("execution(* akka.actor.UnstartedCell.sendMessage(*)) && this(cell) && args(envelope)") 65 | def sendMessageInUnstartedActorCell(cell: Cell, envelope: Envelope): Unit = {} 66 | 67 | 68 | @Before("sendMessageInActorCell(receiver, invocation)") 69 | def beforeSendMessageInActorCell(receiver: ActorCell, invocation: Envelope): Unit = { 70 | setEnvelopeContext(receiver, invocation) 71 | } 72 | 73 | @Before("sendMessageInUnstartedActorCell(cell, envelope)") 74 | def beforeSendMessageInUnstartedActorCell(cell: Cell, envelope: Envelope): Unit = { 75 | setEnvelopeContext(cell, envelope) 76 | } 77 | 78 | private def setEnvelopeContext(cell: Cell, envelope: Envelope): Unit = { 79 | envelope.asInstanceOf[InstrumentedEnvelope].setEnvelopeContext( 80 | actorInstrumentation(cell).captureEnvelopeContext()) 81 | } 82 | 83 | @Pointcut("execution(* akka.actor.UnstartedCell.replaceWith(*)) && this(unStartedCell) && args(cell)") 84 | def replaceWithInRepointableActorRef(unStartedCell: UnstartedCell, cell: Cell): Unit = {} 85 | 86 | @Around("replaceWithInRepointableActorRef(unStartedCell, cell)") 87 | def aroundReplaceWithInRepointableActorRef(pjp: ProceedingJoinPoint, unStartedCell: UnstartedCell, cell: Cell): Unit = { 88 | import ActorCellInstrumentation._ 89 | // TODO: Find a way to do this without resorting to reflection and, even better, without copy/pasting the Akka Code! 90 | val queue = unstartedCellQueueField.get(unStartedCell).asInstanceOf[java.util.LinkedList[_]] 91 | val lock = unstartedCellLockField.get(unStartedCell).asInstanceOf[ReentrantLock] 92 | 93 | def locked[T](body: => T): T = { 94 | lock.lock() 95 | try body finally lock.unlock() 96 | } 97 | 98 | locked { 99 | try { 100 | while (!queue.isEmpty) { 101 | queue.poll() match { 102 | case s: SystemMessage => cell.sendSystemMessage(s) // TODO: ============= CHECK SYSTEM MESSAGESSSSS ========= 103 | case e: Envelope with InstrumentedEnvelope => cell.sendMessage(e) 104 | case e: Envelope => cell.sendMessage(e) 105 | } 106 | } 107 | } finally { 108 | unStartedCell.self.swapCell(cell) 109 | } 110 | } 111 | } 112 | 113 | @Pointcut("execution(* akka.actor.ActorCell.stop()) && this(cell)") 114 | def actorStop(cell: ActorCell): Unit = {} 115 | 116 | @After("actorStop(cell)") 117 | def afterStop(cell: ActorCell): Unit = { 118 | actorInstrumentation(cell).cleanup() 119 | 120 | // The Stop can't be captured from the RoutedActorCell so we need to put this piece of cleanup here. 121 | if (cell.isInstanceOf[RoutedActorCell]) { 122 | cell.asInstanceOf[RouterInstrumentationAware].routerInstrumentation.cleanup() 123 | } 124 | } 125 | 126 | @Pointcut("execution(* akka.actor.ActorCell.handleInvokeFailure(..)) && this(cell) && args(childrenNotToSuspend, failure)") 127 | def actorInvokeFailure(cell: ActorCell, childrenNotToSuspend: immutable.Iterable[ActorRef], failure: Throwable): Unit = {} 128 | 129 | @Before("actorInvokeFailure(cell, childrenNotToSuspend, failure)") 130 | def beforeInvokeFailure(cell: ActorCell, childrenNotToSuspend: immutable.Iterable[ActorRef], failure: Throwable): Unit = { 131 | actorInstrumentation(cell).processFailure(failure) 132 | } 133 | } 134 | 135 | object ActorCellInstrumentation { 136 | private val (unstartedCellQueueField, unstartedCellLockField) = { 137 | val unstartedCellClass = classOf[UnstartedCell] 138 | val queueFieldName = "queue" 139 | 140 | val queueField = unstartedCellClass.getDeclaredField(queueFieldName) 141 | queueField.setAccessible(true) 142 | 143 | val lockField = unstartedCellClass.getDeclaredField("lock") 144 | lockField.setAccessible(true) 145 | 146 | (queueField, lockField) 147 | } 148 | 149 | } 150 | -------------------------------------------------------------------------------- /src/main/scala/akka/monitor/instrumentation/ActorMonitor.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2017,2018 Workday, Inc. 4 | * Copyright © 2013-2017 the kamon project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 7 | * except in compliance with the License. You may obtain a copy of 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 distributed under the 12 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific language governing permissions 14 | * and limitations under the License. 15 | * ========================================================================================= 16 | */ 17 | package akka.monitor.instrumentation 18 | 19 | import java.util.concurrent.TimeUnit 20 | 21 | import org.aspectj.lang.ProceedingJoinPoint 22 | import org.slf4j.LoggerFactory 23 | 24 | import akka.actor.{ActorRef, ActorSystem, Cell} 25 | import akka.monitor.instrumentation.ActorMonitors.{TrackedActor, TrackedRoutee} 26 | import io.kontainers.micrometer.akka._ 27 | 28 | trait ActorMonitor { 29 | def captureEnvelopeContext(): EnvelopeContext 30 | def processMessage(pjp: ProceedingJoinPoint, envelopeContext: EnvelopeContext): AnyRef 31 | def processFailure(failure: Throwable): Unit 32 | def cleanup(): Unit 33 | } 34 | 35 | object ActorMonitor { 36 | 37 | def createActorMonitor(cell: Cell, system: ActorSystem, ref: ActorRef, parent: ActorRef, actorCellCreation: Boolean): ActorMonitor = { 38 | val cellInfo = CellInfo.cellInfoFor(cell, system, ref, parent, actorCellCreation) 39 | 40 | if (cellInfo.isRouter) 41 | ActorMonitors.ContextPropagationOnly 42 | else { 43 | if (cellInfo.isRoutee && cellInfo.isTracked) 44 | createRouteeMonitor(cellInfo) 45 | else 46 | createRegularActorMonitor(cellInfo) 47 | } 48 | } 49 | 50 | def createRegularActorMonitor(cellInfo: CellInfo): ActorMonitor = { 51 | val actorMetrics = if (cellInfo.isTracked) ActorMetrics.metricsFor(cellInfo.entity) else None 52 | new TrackedActor(cellInfo.entity, cellInfo.actorSystemName, actorMetrics, cellInfo.trackingGroups, cellInfo.actorCellCreation) 53 | } 54 | 55 | def createRouteeMonitor(cellInfo: CellInfo): ActorMonitor = { 56 | RouterMetrics.metricsFor(cellInfo.entity) match { 57 | case Some(rm) => 58 | new TrackedRoutee(cellInfo.entity, cellInfo.actorSystemName, rm, cellInfo.trackingGroups, cellInfo.actorCellCreation) 59 | case _ => 60 | new TrackedActor(cellInfo.entity, cellInfo.actorSystemName, None, cellInfo.trackingGroups, cellInfo.actorCellCreation) 61 | } 62 | } 63 | } 64 | 65 | object ActorMonitors { 66 | 67 | val logger = LoggerFactory.getLogger(ActorMonitors.getClass) 68 | 69 | val ContextPropagationOnly = new ActorMonitor { 70 | def captureEnvelopeContext(): EnvelopeContext = EnvelopeContext() 71 | 72 | def processMessage(pjp: ProceedingJoinPoint, envelopeContext: EnvelopeContext): AnyRef = { 73 | pjp.proceed() 74 | } 75 | 76 | def processFailure(failure: Throwable): Unit = {} 77 | def cleanup(): Unit = {} 78 | } 79 | 80 | class TrackedActor(val entity: Entity, actorSystemName: String, actorMetrics: Option[ActorMetrics], 81 | trackingGroups: List[String], actorCellCreation: Boolean) 82 | extends GroupMetricsTrackingActor(entity, actorSystemName, trackingGroups, actorCellCreation) { 83 | 84 | if (logger.isDebugEnabled()) { 85 | logger.debug(s"tracking ${entity.name} actor: ${actorMetrics.isDefined} actor-group: ${trackingGroups}") 86 | } 87 | 88 | override def captureEnvelopeContext(): EnvelopeContext = { 89 | actorMetrics.foreach { am => 90 | am.mailboxSize.increment() 91 | am.messages.increment() 92 | } 93 | super.captureEnvelopeContext() 94 | } 95 | 96 | def processMessage(pjp: ProceedingJoinPoint, envelopeContext: EnvelopeContext): AnyRef = { 97 | val timeInMailbox: Long = System.nanoTime() - envelopeContext.nanoTime 98 | 99 | val actorProcessingTimers = actorMetrics.map { am => 100 | am.processingTime.startTimer() 101 | } 102 | val actorGroupProcessingTimers = trackingGroups.map { group => 103 | ActorGroupMetrics.processingTime(group).startTimer() 104 | } 105 | 106 | try { 107 | pjp.proceed() 108 | } finally { 109 | actorProcessingTimers.foreach { _.close() } 110 | actorGroupProcessingTimers.foreach { _.close() } 111 | 112 | actorMetrics.foreach { am => 113 | am.timeInMailbox.timer.record(timeInMailbox, TimeUnit.NANOSECONDS) 114 | am.mailboxSize.decrement() 115 | } 116 | recordGroupMetrics(timeInMailbox) 117 | } 118 | } 119 | 120 | override def processFailure(failure: Throwable): Unit = { 121 | actorMetrics.foreach { am => 122 | am.errors.increment() 123 | } 124 | super.processFailure(failure) 125 | } 126 | } 127 | 128 | class TrackedRoutee(val entity: Entity, actorSystemName: String, routerMetrics: RouterMetrics, 129 | trackingGroups: List[String], actorCellCreation: Boolean) 130 | extends GroupMetricsTrackingActor(entity, actorSystemName, trackingGroups, actorCellCreation) { 131 | 132 | if (logger.isDebugEnabled()) { 133 | logger.debug(s"tracking ${entity.name} router: true actor-group: ${trackingGroups} actorCellCreation: ${actorCellCreation}") 134 | } 135 | 136 | override def captureEnvelopeContext(): EnvelopeContext = { 137 | routerMetrics.messages.increment() 138 | super.captureEnvelopeContext() 139 | } 140 | 141 | def processMessage(pjp: ProceedingJoinPoint, envelopeContext: EnvelopeContext): AnyRef = { 142 | val timeInMailbox: Long = System.nanoTime() - envelopeContext.nanoTime 143 | 144 | val processingTimer = routerMetrics.processingTime.startTimer() 145 | val actorGroupProcessingTimers = trackingGroups.map { group => 146 | ActorGroupMetrics.processingTime(group).startTimer() 147 | } 148 | 149 | try { 150 | pjp.proceed() 151 | } finally { 152 | processingTimer.close() 153 | actorGroupProcessingTimers.foreach { _.close() } 154 | routerMetrics.timeInMailbox.timer.record(timeInMailbox, TimeUnit.NANOSECONDS) 155 | recordGroupMetrics(timeInMailbox) 156 | } 157 | } 158 | 159 | override def processFailure(failure: Throwable): Unit = { 160 | routerMetrics.errors.increment() 161 | super.processFailure(failure) 162 | } 163 | } 164 | 165 | abstract class GroupMetricsTrackingActor(entity: Entity, actorSystemName: String, 166 | trackingGroups: List[String], actorCellCreation: Boolean) extends ActorMonitor { 167 | if (actorCellCreation) { 168 | ActorSystemMetrics.actorCount(actorSystemName).increment() 169 | trackingGroups.foreach { group => 170 | ActorGroupMetrics.actorCount(group).increment() 171 | } 172 | } 173 | 174 | def captureEnvelopeContext(): EnvelopeContext = { 175 | trackingGroups.foreach { group => 176 | ActorGroupMetrics.mailboxSize(group).increment() 177 | ActorGroupMetrics.messages(group).increment() 178 | } 179 | EnvelopeContext() 180 | } 181 | 182 | protected def recordGroupMetrics(timeInMailbox: Long): Unit = { 183 | trackingGroups.foreach { group => 184 | ActorGroupMetrics.timeInMailbox(group).timer.record(timeInMailbox, TimeUnit.NANOSECONDS) 185 | ActorGroupMetrics.mailboxSize(group).decrement() 186 | } 187 | } 188 | 189 | def processFailure(failure: Throwable): Unit = { 190 | trackingGroups.foreach { group => 191 | ActorGroupMetrics.errors(group).increment() 192 | } 193 | } 194 | 195 | def cleanup(): Unit = { 196 | if (actorCellCreation) { 197 | ActorSystemMetrics.actorCount(actorSystemName).decrement() 198 | trackingGroups.foreach { group => 199 | ActorGroupMetrics.actorCount(group).decrement() 200 | } 201 | } 202 | } 203 | 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/main/scala/akka/monitor/instrumentation/DispatcherInstrumentation.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2017,2018 Workday, Inc. 4 | * Copyright © 2013-2017 the kamon project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 7 | * except in compliance with the License. You may obtain a copy of 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 distributed under the 12 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific language governing permissions 14 | * and limitations under the License. 15 | * ========================================================================================= 16 | */ 17 | 18 | package akka.monitor.instrumentation 19 | 20 | import java.lang.reflect.Method 21 | import java.util.concurrent.{ExecutorService, ForkJoinPool, ThreadPoolExecutor} 22 | 23 | import akka.actor.{ActorContext, ActorSystem, ActorSystemImpl, Props} 24 | import akka.dispatch.{Dispatcher, Dispatchers, ExecutorServiceDelegate, MessageDispatcher} 25 | import akka.monitor.instrumentation.LookupDataAware.LookupData 26 | import io.kontainers.micrometer.akka.{AkkaMetricRegistry, ForkJoinPoolLike, ForkJoinPoolMetrics, MetricsConfig, ThreadPoolMetrics} 27 | import io.micrometer.core.instrument.Tag 28 | import io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics 29 | import org.aspectj.lang.ProceedingJoinPoint 30 | import org.aspectj.lang.annotation._ 31 | import org.slf4j.LoggerFactory 32 | 33 | import scala.util.control.NonFatal 34 | 35 | @Aspect 36 | class DispatcherInstrumentation { 37 | 38 | val logger = LoggerFactory.getLogger(classOf[DispatcherInstrumentation]) 39 | 40 | @Pointcut("execution(* akka.actor.ActorSystemImpl.start(..)) && this(system)") 41 | def actorSystemInitialization(system: ActorSystemImpl): Unit = {} 42 | 43 | @Before("actorSystemInitialization(system)") 44 | def afterActorSystemInitialization(system: ActorSystemImpl): Unit = { 45 | system.dispatchers.asInstanceOf[ActorSystemAware].actorSystem = system 46 | 47 | // The default dispatcher for the actor system is looked up in the ActorSystemImpl's initialization code and we 48 | // can't get the Metrics extension there since the ActorSystem is not yet fully constructed. To workaround that 49 | // we are manually selecting and registering the default dispatcher with the Metrics extension. All other dispatchers 50 | // will by registered by the instrumentation below. 51 | 52 | // Yes, reflection sucks, but this piece of code is only executed once on ActorSystem's startup. 53 | val defaultDispatcher = system.dispatcher 54 | val defaultDispatcherExecutor = extractExecutor(defaultDispatcher.asInstanceOf[MessageDispatcher]) 55 | registerDispatcher(Dispatchers.DefaultDispatcherId, defaultDispatcherExecutor, Some(system)) 56 | } 57 | 58 | private def extractExecutor(dispatcher: MessageDispatcher): ExecutorService = { 59 | val executorServiceMethod: Method = { 60 | // executorService is protected 61 | val method = classOf[Dispatcher].getDeclaredMethod("executorService") 62 | method.setAccessible(true) 63 | method 64 | } 65 | 66 | dispatcher match { 67 | case x: Dispatcher => 68 | val executor = executorServiceMethod.invoke(x) match { 69 | case delegate: ExecutorServiceDelegate => delegate.executor 70 | case other => other 71 | } 72 | executor.asInstanceOf[ExecutorService] 73 | } 74 | } 75 | 76 | private def registerDispatcher(dispatcherName: String, executorService: ExecutorService, 77 | system: Option[ActorSystem]): Unit = { 78 | val prefixedName = system match { 79 | case Some(s) => s"${s.name}_${dispatcherName}" 80 | case None => dispatcherName 81 | } 82 | registerDispatcher(prefixedName, executorService) 83 | } 84 | 85 | private def registerDispatcher(prefixedName: String, executorService: ExecutorService): Unit = { 86 | if (MetricsConfig.shouldTrack(MetricsConfig.Dispatcher, prefixedName)) { 87 | if (MetricsConfig.useMicrometerExecutorServiceMetrics) { 88 | executorService match { 89 | case tpe: ThreadPoolExecutor => ExecutorServiceMetrics.monitor(AkkaMetricRegistry.getRegistry, tpe, prefixedName, Tag.of("type", "ThreadPoolExecutor")) 90 | case fjp: ForkJoinPool => ExecutorServiceMetrics.monitor(AkkaMetricRegistry.getRegistry, fjp, prefixedName, Tag.of("type", "ForkJoinPool")) 91 | case _ => 92 | ExecutorServiceMetrics.monitor(AkkaMetricRegistry.getRegistry, executorService, prefixedName, Tag.of("type", "unknown")) 93 | } 94 | } else { 95 | executorService match { 96 | case tpe: ThreadPoolExecutor => ThreadPoolMetrics.add(prefixedName, tpe) 97 | case other => { 98 | try { 99 | val fjp = executorService.asInstanceOf[ForkJoinPoolLike] 100 | ForkJoinPoolMetrics.add(prefixedName, fjp) 101 | } catch { 102 | case NonFatal(e) => logger.warn(s"Unhandled Dispatcher Execution Service ${other.getClass.getName}") 103 | } 104 | } 105 | } 106 | } 107 | } 108 | } 109 | 110 | @Pointcut("execution(* akka.dispatch.Dispatchers.lookup(..)) && this(dispatchers) && args(dispatcherName)") 111 | def dispatchersLookup(dispatchers: ActorSystemAware, dispatcherName: String) = {} 112 | 113 | @Around("dispatchersLookup(dispatchers, dispatcherName)") 114 | def aroundDispatchersLookup(pjp: ProceedingJoinPoint, dispatchers: ActorSystemAware, dispatcherName: String): Any = 115 | LookupDataAware.withLookupData(LookupData(dispatcherName, dispatchers.actorSystem)) { 116 | pjp.proceed() 117 | } 118 | 119 | @Pointcut("initialization(akka.dispatch.ExecutorServiceFactory.new(..)) && target(factory)") 120 | def executorServiceFactoryInitialization(factory: LookupDataAware): Unit = {} 121 | 122 | @After("executorServiceFactoryInitialization(factory)") 123 | def afterExecutorServiceFactoryInitialization(factory: LookupDataAware): Unit = 124 | factory.lookupData = LookupDataAware.currentLookupData 125 | 126 | @Pointcut("execution(* akka.dispatch.ExecutorServiceFactory+.createExecutorService()) && this(factory) && !cflow(execution(* akka.dispatch.Dispatcher.shutdown()))") 127 | def createExecutorService(factory: LookupDataAware): Unit = {} 128 | 129 | @AfterReturning(pointcut = "createExecutorService(factory)", returning = "executorService") 130 | def afterCreateExecutorService(factory: LookupDataAware, executorService: ExecutorService): Unit = { 131 | val lookupData = factory.lookupData 132 | 133 | // lookupData.actorSystem will be null only during the first lookup of the default dispatcher during the 134 | // ActorSystemImpl's initialization. 135 | if (lookupData.actorSystem != null) 136 | registerDispatcher(lookupData.dispatcherName, executorService, None) 137 | } 138 | 139 | @Pointcut("initialization(akka.dispatch.Dispatcher.LazyExecutorServiceDelegate.new(..)) && this(lazyExecutor)") 140 | def lazyExecutorInitialization(lazyExecutor: LookupDataAware): Unit = {} 141 | 142 | @After("lazyExecutorInitialization(lazyExecutor)") 143 | def afterLazyExecutorInitialization(lazyExecutor: LookupDataAware): Unit = 144 | lazyExecutor.lookupData = LookupDataAware.currentLookupData 145 | 146 | @Pointcut("execution(* akka.dispatch.Dispatcher.LazyExecutorServiceDelegate.copy()) && this(lazyExecutor)") 147 | def lazyExecutorCopy(lazyExecutor: LookupDataAware): Unit = {} 148 | 149 | @Around("lazyExecutorCopy(lazyExecutor)") 150 | def aroundLazyExecutorCopy(pjp: ProceedingJoinPoint, lazyExecutor: LookupDataAware): Any = 151 | LookupDataAware.withLookupData(lazyExecutor.lookupData) { 152 | pjp.proceed() 153 | } 154 | 155 | @Pointcut("execution(* akka.dispatch.Dispatcher.LazyExecutorServiceDelegate.shutdown()) && this(lazyExecutor)") 156 | def lazyExecutorShutdown(lazyExecutor: LookupDataAware): Unit = {} 157 | 158 | @After("lazyExecutorShutdown(lazyExecutor)") 159 | def afterLazyExecutorShutdown(lazyExecutor: LookupDataAware): Unit = {} 160 | 161 | @Pointcut("execution(* akka.routing.BalancingPool.newRoutee(..)) && args(props, context)") 162 | def createNewRouteeOnBalancingPool(props: Props, context: ActorContext): Unit = {} 163 | 164 | @Around("createNewRouteeOnBalancingPool(props, context)") 165 | def aroundCreateNewRouteeOnBalancingPool(pjp: ProceedingJoinPoint, props: Props, context: ActorContext): Any = { 166 | val deployPath = context.self.path.elements.drop(1).mkString("/", "/", "") 167 | val dispatcherId = s"BalancingPool-$deployPath" 168 | 169 | LookupDataAware.withLookupData(LookupData(dispatcherId, context.system)) { 170 | pjp.proceed() 171 | } 172 | } 173 | } 174 | 175 | @Aspect 176 | class DispatcherMetricCollectionInfoIntoDispatcherMixin { 177 | 178 | @DeclareMixin("akka.dispatch.Dispatchers") 179 | def mixinActorSystemAwareToDispatchers: ActorSystemAware = ActorSystemAware() 180 | 181 | @DeclareMixin("akka.dispatch.Dispatcher.LazyExecutorServiceDelegate") 182 | def mixinLookupDataAwareToExecutors: LookupDataAware = LookupDataAware() 183 | 184 | @DeclareMixin("akka.dispatch.ExecutorServiceFactory+") 185 | def mixinActorSystemAwareToDispatcher: LookupDataAware = LookupDataAware() 186 | } 187 | 188 | trait ActorSystemAware { 189 | @volatile var actorSystem: ActorSystem = _ 190 | } 191 | 192 | object ActorSystemAware { 193 | def apply(): ActorSystemAware = new ActorSystemAware {} 194 | } 195 | 196 | trait LookupDataAware { 197 | @volatile var lookupData: LookupData = _ 198 | } 199 | 200 | object LookupDataAware { 201 | case class LookupData(dispatcherName: String, actorSystem: ActorSystem) 202 | 203 | private val _currentDispatcherLookupData = new ThreadLocal[LookupData] 204 | 205 | def apply() = new LookupDataAware {} 206 | 207 | def currentLookupData: LookupData = _currentDispatcherLookupData.get() 208 | 209 | def withLookupData[T](lookupData: LookupData)(thunk: => T): T = { 210 | _currentDispatcherLookupData.set(lookupData) 211 | val result = thunk 212 | _currentDispatcherLookupData.remove() 213 | 214 | result 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | --------------------------------------------------------------------------------