Skip to content

Commit 8f95141

Browse files
authored
Merge pull request #1051 from SimunKaracic/initial-caffeine-implementation
Initial caffeine implementation
2 parents f346163 + e0548c7 commit 8f95141

File tree

6 files changed

+253
-1
lines changed

6 files changed

+253
-1
lines changed

build.sbt

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ val instrumentationProjects = Seq[ProjectReference](
132132
`kamon-okhttp`,
133133
`kamon-tapir`,
134134
`kamon-redis`,
135+
`kamon-caffeine`,
135136
)
136137

137138
lazy val instrumentation = (project in file("instrumentation"))
@@ -516,6 +517,20 @@ lazy val `kamon-redis` = (project in file("instrumentation/kamon-redis"))
516517
)
517518
).dependsOn(`kamon-core`, `kamon-testkit` % "test")
518519

520+
lazy val `kamon-caffeine` = (project in file("instrumentation/kamon-caffeine"))
521+
.disablePlugins(AssemblyPlugin)
522+
.enablePlugins(JavaAgent)
523+
.settings(instrumentationSettings)
524+
.settings(
525+
libraryDependencies ++= Seq(
526+
kanelaAgent % "provided",
527+
"com.github.ben-manes.caffeine" % "caffeine" % "2.8.5" % "provided",
528+
529+
scalatest % "test",
530+
logbackClassic % "test",
531+
)
532+
).dependsOn(`kamon-core`, `kamon-testkit` % "test")
533+
519534
/**
520535
* Reporters
521536
*/
@@ -756,7 +771,8 @@ val `kamon-bundle` = (project in file("bundle/kamon-bundle"))
756771
`kamon-play` % "shaded",
757772
`kamon-redis` % "shaded",
758773
`kamon-okhttp` % "shaded",
759-
)
774+
`kamon-caffeine` % "shaded",
775+
)
760776

761777
lazy val `bill-of-materials` = (project in file("bill-of-materials"))
762778
.enablePlugins(BillOfMaterialsPlugin)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Metrics are gathered using the KamonStatsCounter, which needs to be added manually
2+
# e.g. Caffeine.newBuilder()
3+
# .recordStats(() -> new KamonStatsCounter("cache_name"))
4+
# .build();
5+
6+
7+
kanela.modules {
8+
caffeine {
9+
name = "Caffeine instrumentation"
10+
description = "Provides tracing and stats for synchronous cache operations"
11+
12+
instrumentations = [
13+
"kamon.instrumentation.caffeine.CaffeineCacheInstrumentation"
14+
]
15+
16+
within = [
17+
"com.github.benmanes.caffeine.cache.LocalCache",
18+
"com.github.benmanes.caffeine.cache.LocalManualCache",
19+
]
20+
}
21+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package kamon.instrumentation.caffeine
2+
3+
import kamon.Kamon
4+
import kamon.trace.Span
5+
import kanela.agent.api.instrumentation.InstrumentationBuilder
6+
import kanela.agent.libs.net.bytebuddy.asm.Advice
7+
8+
class CaffeineCacheInstrumentation extends InstrumentationBuilder {
9+
onType("com.github.benmanes.caffeine.cache.LocalCache")
10+
.advise(method("computeIfAbsent"), classOf[SyncCacheAdvice])
11+
.advise(method("getIfPresent"), classOf[GetIfPresentAdvice])
12+
13+
onType("com.github.benmanes.caffeine.cache.LocalManualCache")
14+
.advise(method("getAll"), classOf[SyncCacheAdvice])
15+
.advise(method("put"), classOf[SyncCacheAdvice])
16+
.advise(method("getIfPresent"), classOf[GetIfPresentAdvice])
17+
.advise(method("putAll"), classOf[SyncCacheAdvice])
18+
.advise(method("getAllPresent"), classOf[SyncCacheAdvice])
19+
}
20+
21+
class SyncCacheAdvice
22+
object SyncCacheAdvice {
23+
@Advice.OnMethodEnter()
24+
def enter(@Advice.Origin("#m") methodName: String) = {
25+
Kamon.clientSpanBuilder(s"caffeine.$methodName", "caffeine").start()
26+
}
27+
28+
@Advice.OnMethodExit(suppress = classOf[Throwable])
29+
def exit(@Advice.Enter span: Span): Unit = {
30+
span.finish()
31+
}
32+
}
33+
34+
class GetIfPresentAdvice
35+
object GetIfPresentAdvice {
36+
@Advice.OnMethodEnter()
37+
def enter(@Advice.Origin("#m") methodName: String) = {
38+
Kamon.clientSpanBuilder(s"caffeine.$methodName", "caffeine").start()
39+
}
40+
41+
@Advice.OnMethodExit(suppress = classOf[Throwable])
42+
def exit(@Advice.Enter span: Span,
43+
@Advice.Return ret: Any,
44+
@Advice.Argument(0) key: Any): Unit = {
45+
if (ret == null) {
46+
span.tag("cache.miss", s"No value for key $key")
47+
}
48+
span.finish()
49+
}
50+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package kamon.instrumentation.caffeine
2+
3+
import com.github.benmanes.caffeine.cache.RemovalCause
4+
import com.github.benmanes.caffeine.cache.stats.{CacheStats, StatsCounter}
5+
import kamon.Kamon
6+
7+
class KamonStatsCounter(name: String) extends StatsCounter {
8+
val hitsCounter = Kamon.counter(s"cache.${name}.hits").withoutTags()
9+
val missesCounter = Kamon.counter(s"cache.${name}.misses").withoutTags()
10+
val evictionCount = Kamon.counter(s"cache.${name}.evictions").withoutTags()
11+
val loadSuccessTime = Kamon.timer(s"cache.${name}.load-time.success").withoutTags()
12+
val loadFailureTime = Kamon.timer(s"cache.${name}.load-time.failure").withoutTags()
13+
val evictionWeight = Kamon.counter(s"cache.${name}.eviction.weight")
14+
val evictionWeightInstruments = RemovalCause.values()
15+
.map(cause => cause -> evictionWeight.withTag("eviction.cause", cause.name()))
16+
.toMap
17+
18+
override def recordHits(count: Int): Unit = hitsCounter.increment(count)
19+
20+
override def recordMisses(count: Int): Unit = missesCounter.increment(count)
21+
22+
override def recordLoadSuccess(loadTime: Long): Unit = loadSuccessTime.record(loadTime)
23+
24+
override def recordLoadFailure(loadTime: Long): Unit = loadFailureTime.record(loadTime)
25+
26+
27+
override def recordEviction(): Unit = {
28+
evictionCount.increment()
29+
}
30+
31+
override def recordEviction(weight: Int): Unit = {
32+
evictionCount.increment()
33+
evictionWeight.withoutTags().increment(weight)
34+
}
35+
36+
override def recordEviction(weight: Int, cause: RemovalCause): Unit = {
37+
evictionCount.increment()
38+
evictionWeightInstruments.get(cause).map(_.increment(weight))
39+
}
40+
41+
/**
42+
* Overrides the snapshot method and returns stubbed CacheStats.
43+
* When using KamonStatsCounter, it is assumed that you are using a
44+
* reporter, and are not going to be printing or logging the stats.
45+
*/
46+
override def snapshot() = new CacheStats(0, 0, 0, 0, 0, 0, 0)
47+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package kamon.instrumentation.caffeine
2+
3+
import com.github.benmanes.caffeine.cache.{AsyncCache, Caffeine}
4+
import kamon.testkit.TestSpanReporter
5+
import org.scalatest.concurrent.Eventually.eventually
6+
import org.scalatest.concurrent.Waiters.timeout
7+
import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpec}
8+
9+
import java.util
10+
import java.util.concurrent.CompletableFuture
11+
import scala.collection.JavaConverters._
12+
import scala.concurrent.duration.DurationInt
13+
14+
class CaffeineAsyncCacheSpec
15+
extends WordSpec
16+
with Matchers
17+
with BeforeAndAfterAll
18+
with TestSpanReporter {
19+
"Caffeine instrumentation for async caches" should {
20+
val cache: AsyncCache[String, String] = Caffeine.newBuilder()
21+
.buildAsync[String, String]()
22+
23+
"not create a span when using put" in {
24+
cache.put("a", CompletableFuture.completedFuture("key"))
25+
eventually(timeout(2.seconds)) {
26+
testSpanReporter().spans() shouldBe empty
27+
}
28+
}
29+
30+
"not create a span when using get" in {
31+
cache.get("a", new java.util.function.Function[String, String] {
32+
override def apply(a: String): String = "value"
33+
})
34+
eventually(timeout(2.seconds)) {
35+
testSpanReporter().spans() shouldBe empty
36+
}
37+
}
38+
39+
"not create a span when using getIfPresent" in {
40+
cache.getIfPresent("not_exists")
41+
eventually(timeout(2.seconds)) {
42+
testSpanReporter().spans() shouldBe empty
43+
}
44+
}
45+
}
46+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package kamon.instrumentation.caffeine
2+
3+
import com.github.benmanes.caffeine.cache.{Cache, Caffeine}
4+
import kamon.testkit.TestSpanReporter
5+
import org.scalatest.OptionValues.convertOptionToValuable
6+
import org.scalatest.concurrent.Eventually.eventually
7+
import org.scalatest.concurrent.Waiters.timeout
8+
import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpec}
9+
10+
import scala.collection.JavaConverters._
11+
import scala.concurrent.duration.DurationInt
12+
13+
class CaffeineSyncCacheSpec
14+
extends WordSpec
15+
with Matchers
16+
with BeforeAndAfterAll
17+
with TestSpanReporter {
18+
"Caffeine instrumentation for sync caches" should {
19+
val cache: Cache[String, String] = Caffeine.newBuilder()
20+
.build[String, String]()
21+
22+
"create a span when putting a value" in {
23+
cache.put("a", "key")
24+
eventually(timeout(2.seconds)) {
25+
val span = testSpanReporter().nextSpan().value
26+
span.operationName shouldBe "caffeine.put"
27+
testSpanReporter().spans() shouldBe empty
28+
}
29+
}
30+
31+
"create a span when accessing an existing key" in {
32+
cache.get("a", new java.util.function.Function[String, String] {
33+
override def apply(a: String): String = "value"
34+
})
35+
eventually(timeout(2.seconds)) {
36+
val span = testSpanReporter().nextSpan().value
37+
span.operationName shouldBe "caffeine.computeIfAbsent"
38+
testSpanReporter().spans() shouldBe empty
39+
}
40+
}
41+
42+
"create only one span when using putAll" in {
43+
val map = Map("b" -> "value", "c" -> "value").asJava
44+
cache.putAll(map)
45+
eventually(timeout(2.seconds)) {
46+
val span = testSpanReporter().nextSpan().value
47+
span.operationName shouldBe "caffeine.putAll"
48+
testSpanReporter().spans() shouldBe empty
49+
testSpanReporter().clear()
50+
}
51+
}
52+
53+
"create a tagged span when accessing a key that does not exist" in {
54+
cache.getIfPresent("not_exists")
55+
eventually(timeout(2.seconds)) {
56+
val span = testSpanReporter().nextSpan().value
57+
span.operationName shouldBe "caffeine.getIfPresent"
58+
span.tags.all().foreach(_.key shouldBe "cache.miss")
59+
testSpanReporter().spans() shouldBe empty
60+
}
61+
}
62+
63+
"create a span when using getAllPresent" in {
64+
cache.getAllPresent(Seq("a", "b").asJava)
65+
eventually(timeout(2.seconds)) {
66+
val span = testSpanReporter().nextSpan().value
67+
span.operationName shouldBe "caffeine.getAllPresent"
68+
testSpanReporter().spans() shouldBe empty
69+
}
70+
}
71+
}
72+
}

0 commit comments

Comments
 (0)