├── 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 | [](https://travis-ci.org/kontainers/micrometer-akka)
2 | [](https://maven-badges.herokuapp.com/maven-central/io.kontainers/micrometer-akka_2.13)
3 | [](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 |
--------------------------------------------------------------------------------